文章目录
0. 前言第二课 - 头文件与类的声明第三课-构造函数第四课-函数传递与返回值第五课-操作符重载与临时对象第六课-复习Complex类的实现过程第七课-拷贝构造,构造复制,析构第八课-堆,栈与内存管理第九课-复习String类的实现过程第十课-扩展补充:类模版,函数模版,及其他0. 前言
侯捷大佬所有C++课程之一
全部课程参考这里
本文对应的课程: 面向过程
包括第二课到第十课,相关内容主要是以实现 Complex 类与 String 类为目标介绍了类创建的基本语法、思路
一些原则:
构造函数使用 intialize list(即冒号)成员函数尽可能添加 const(即常量成员函数)参数传递尽量 pass by reference函数返回值尽量 return by reference函数绝大部分在 public,成员变量尽可能放在 private 中
第二课 - 头文件与类的声明
区分基于对象(Object Based)和面向对象(Object Oriented)C++代码基本形式:.h(自己写/第三方的头文件) + .cpp(自己写/第三方的源码) + .h(标准代码库,编译器提供)
头文件防卫式声明,即#ifndef/#define/#endif
所有头文件都应该使用这种结构
头文件布局
class 的声明(即上图中的1
部分),设计有哪些数据、哪些操作class template 简介,即模版 如一个复数类(Complex),拥有两个参数,分别表示实部与虚部。实部与虚部的数据类型不确定,可能都是 int、都是 float、都是 double。为了实现上面这个需求,就可以使用模版类。
第三课-构造函数
inline(内联)函数
在函数体(class body)内定义的函数就是内联函数函数体外定义的函数,可以使用 inline 关键字修饰,成为内联函数inline 能提高运行速度理想状态下,所有函数都是inline就最好了,但编译器不允许。代码中定义的inline function只是对编译器的建议,最终是否真正成为inline看编译器选择。
访问级别:private/protected/public,可以交叉、重复出现
构造函数
函数名与类名相同,且没有返回类型,用于创建对象的实例只用于创建对象,已经创建好的对象不能调用尽可能使用分号构造函数形式,效率高一些构造函数位于 private 中的内容由于有默认参数,所以可能会出现歧义,这时候就会导致编译错误。
重载(overloading):函数名相同,参数不同,C语言中不支持这种语法。
实现原理:编译器中的函数名包括了参数数量、参数名称等
第四课-函数传递与返回值
构造函数放在 private 区域:不允许别人构造函数,如单例模式
常量成员函数(如double real() const {return re;}
):
定义:不改变成员变量数值在定义和声明中都应加const限定常量(const对象)对象只能调用常量成员函数
函数传递
按值传递(by value):整包传递引用传递(by reference): C中使用指针(4个字节,存储地址空间)C++中使用引用(底层实现是指针,性能就像指针,但形式更漂亮)修改引用中的参数就会导致外部对象的内容改变,如果不希望这样,可以使用 const referencereference 的主要作用就是参数值传递和返回值传递使用 reference 的主要原因是效率
返回值传递:也分为 by value 和 by reference,尽可能使用后者
友元(friend)
友元函数:可以调用形参的非 public 成员变量友元不利于“封装”特性相同 class 的各个 objects 互为友元
类的总体设计原则
数据全部是 private传参尽可能使用 reference,需要不需要 const 看情况返回值尽可能使用 reference成员函数尽可能使用 const 修饰构造函数使用 initialization list(即冒号)
不可以使用 return by reference 的情况
在函数体内部创建了一个对象作为函数的返回值时,不能传递这个对象的引用因为是局部变量,函数运行完了对象就销毁了,对象的引用就没有意义传递着无需知道接受者是以 reference 接收(如果是指针,就必须先确定是指针)
第五课-操作符重载与临时对象
操作符重载-成员函数
成员函数有默认形参this
,编译器会自动添加到成员函数的形参中(位置不一定,根据成员函数确定)如果只有c2 += c1
这种用法,那么对应的函数实现为void complex::operator += (const complex &r)
就可以了,注意,返回值类型是 void。如果要支持c3 += c2 += c1
,那么返回值类型必须是complex&
,即complex& complex::operator += (const complex& r)
操作符重载-非成员函数
全局函数,没有 this,形参包括两个参数返回的是value而不是reference,因为返回的是local object(局部对象)常用的特殊语法typename();
用于创建临时对象,不能用于 returen by reference
complex c1(2, 1);complex c2;// 创建临时对象complex();complex(4, 5);// 上面两个对象到这一步就没有了
第六课-复习Complex类的实现过程
新手比较适合看,我就听了一遍第七课-拷贝构造,构造复制,析构
拷贝构造 定义时形如String(const String& str);
使用时形如String s3(s1);
参数第一次出现 构造复制 定义时形如String& operator=(const String& str);
形如s3 = s2;
参数s3不是第一次出现,之前已经声明过基本构造流程:基本实现: 自我检测(自我赋值),如果没有定义在进行自我赋值(如a = a
)时会报错,因为第一步就是删除内存。自己删除+新建+拷贝 析构 当对象死亡时会自动调用,名称与类名称相同,前面添加波浪号~
定义时形如~String();
一个主要作用是防止内存泄漏。所谓内存泄漏,就是在对象死亡后,其对应的“动态分配”的内存并没有收回。 拷贝构造和拷贝复制的默认实现以及必要性 如果没有自己定义,那编译器会默认实现,默认行为就是按照数值一个一个复制,就是浅拷贝。要不要自己重新实现,主要要关注的是成员变量中是否存在指针。如果有指针就必须自己实现,这是因为: 对于拷贝构造,如果直接复制指针,会导致内存泄漏(被赋值的指针原本指向的地址空间没人处理了)如下图所示,第一行是定义了两个String对象,第二行就是默认拷贝赋值操作,可以看到这只是浅拷贝,有内存泄漏产生。// 拷贝构造的形式(下面三个都是)String s1("hello");String s2(s1);String s3 = s1;// 因为创建了新的对象,所以就是利用构造函数实现
字符串的两种设计 PASCAL:前面有常数,指定字符数量,后面跟着字符数量C/C++:以特定字符(如\0
)结尾
第八课-堆,栈与内存管理
String 重载<<
符号必须使用全局函数,而不是成员函数
如果是成员函数,那么在使用过程中 cout 和 String 对象的位置是相反的,即str << cout
全局函数参数分为两个,一个是 ostream,令一个就是 String 对象为了实现连续操作(如out << str1 << str2
),重载函数的返回值需要是ostream&
的形式
堆与栈的定义
栈(stack)是存在于某作用域(scope)内的一块内存空间(memory space)。
例如,调用函数时,函数内部就会形成一个stack,放置参数、返回值地址。
堆(heap),即system heap,是指由操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从中获取若干块(blocks)
一般都是通过new
来获取内存空间
举例如下:
class Complex {...};...{Complex c1(1, 2); // stackComplex c2 = new Complex(3); // heap}
生命期
stack objects:作用域(scope)结束之后结束,会自动调用析构函数。 作用域内object,就是局部变量,也称为 auto object,所谓 auto 就是会被自动清理 static local objects: 写法:在一个 scope 中的成员变量通过 static 修饰在作用域结束后仍然存在,直到程序结束才会调用析构函数 global objects: 写法:写在任何作用域之外(任何大括号之外)的对象程序结束之后才会结束可以看成是一个static对象 heap objects: 通过new
创建,通过delete
结束如果没有 delete 就会造成内存泄漏所谓内存泄漏(memory leak),就是当作用域结束,p所指的heap object仍然存在,但p的声明周期已经结束,作用域之外再也看不到p(没有机会 delelete p)
new 的细节:先分配memory,再调用构造函数,具体情况参考下图
operator new
是 C++ 提供的特殊函数,函数名中包含了空格,本质就是C++版的 mallocsizeof 就是两个成员变量(即两个double),8字节数据转型,即static_cast
调用构造函数。构造函数是一个函数名称与class名相同的函数。构造函数的全名就是Complex::Complex(pc, 1, 2)
,其中 pc 就是对象本身 delete 的细节:先调用析构函数,再释放内存 析构函数删除指针指向的内容,即动态内存。这个析构函数是自己定义,而不是默认的。operator delete
是C++的一个特殊函数,函数名包括空格,用来释放内存。 动态分配内存的底层细节,即 malloc 和 free 的细节 下图是在VC下的实现,其他编译器可能不完全一样,但也差不了太多最左边是Debug模式下Complex对象的内存分配 红色区域是编译器分配的cookie,首尾各4字节灰色区域是Debug模式特有的,前32字节后4字节绿色区域是实际Complex对象,8字节(两个double对象)深绿色区域是pad,因为VC下分配的内存(memory block)必须是16的倍数,前面全部加起来是52字节,会自动pad到64字节,所以需要增加12字节 第二列是Release模式下Complex对象的内存分配,就是cookie加上Complex对象,即16字节,不需要pad第三列是Debug模式下,String对象的内存分配,灰色区域大小与之前Complex对象不变,String对象本体4字节(只有一个指针),计算后得到48字节,无序pad。第四列是Release模式下,String的内存分配,在计算cookie和本体后得到12字节,需要pad为16字节。cookie的作用:记录对象的内存大小以及状态 比如第一列是41
,这是16进制,对应10进制就是65。其中40
就是字节数。末尾的1表示这一块是给出去(1)还是收回来(0) 数组动态分配内存的底层细节 最左边的介绍一下:首尾两个cookie各4字节,debug 模式相关的是32+4字节,保存数组长度(即图中的3)需要4字节,实际三个Complex对象,需要8*3字节,pad 8字节达到16的倍数char *data = new char[10]
需要搭配delete[] data
,为什么 左边的图,使用delete[] p
就会调用三次析构函数右边的图,使用delete p
,只调用一次析构函数(注意,调用了析构函数)
第九课-复习String类的实现过程
第十课-扩展补充:类模版,函数模版,及其他
static
普通成员函数有隐藏参数 thisstatic 没有隐藏参数 this静态变量要在class定义外定义静态函数可以通过 object 或 class name调用单例模式的实现
cout
cout 是一种 ostream重载了各种类型的<<
方法
类模版class template
namespace