自己写的C++11 Primer Plus 学习笔记,如有雷同不胜荣幸,如有错误敬请指正
1. 类和动态内存分配
1. 动态内存和类
- 类声明没有为字符串本身分配存储空间,而是在构造函数中使用 new 来为字符串分配空间,这避免了在类声明中预定义字符串的长度。
- 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存
- 初始化是在方法文件中,而不是在类声明文件中
- 静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整形或枚举类型,则可以在类声明中初始化
当使用一个对象初始化另一个对象时,编译器将自动生成下述构造函数:
StringBad sail = sports; //sports 是 StringBad 对象
//等价于
StringBad sail = StringBad(sports);复制代码
- 1
- 2
- 3
C++提供了以下成员函数:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
- 移动构造函数
- 移动复制运算符
① 赋值构造函数
赋值构造函数用于将一个对象复制到一个新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程:
Class_name(const Class_name &);
。
当新建一个对象并将新对象初始化为同类现有对象时,复制构造函数将被调用
StringBad ditto(motto); //calls StringBad(const StringBad &)
StringBad metoo = motto; //calls StringBad(const StringBad &)
StringBad also = StringBad(motto); //calls StringBad (const StringBad &)
StringBad * pStringBad = new StringBad(motto); //calls StringBad (const StringBad &)复制代码
- 1
- 2
- 3
- 4
每当程序生成一个对象副本时,编译器都将使用复制构造函数。具体说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。
默认复制构造函数逐个复制非静态成员(成员复制也称浅复制。因为隐式复制构造函数是按值进行复制,这里的复制并不是字符串,而是一个指向字符串的指针),复制的是成员的值
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
因为按值复制只是复制指向字符串的指针,因此使用strcpy()
可以实现深复制
② 赋值运算符
原型: Class_name & Class_name::operator=(const Class_name &)
,它接受并返回一个指向类对象的引用
功能: 将已有的对象赋值给另一个对象时,将使用重载的赋值运算符
由于默认赋值运算符是浅复制,将导致调用析构函数时出现数据受损,解决办法:提供赋值运算符(进行深复制)定义
- 由于目标对象可能引用了以前分配的数据,所以函数应使用
delete[]
来释放这些数据 - 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容
- 函数返回一个指向调用对象的引用
C++11 空指针表示:0 或 NULL 或 nullptr(官方推荐)
静态成员函数: ① 不能通过对象调用静态成员函数;② 静态成员函数不能使用 this
指针;③ 如果静态成员函数是在共有部分声明,则可以使用类名和作用域解析运算符来调用它
③ 注意事项
使用 new 时的注意事项:
- 如果在构造函数中使用 new 来初始化指针成员,则应在析构函数中使用 delete
- new 和 delete 必须相互兼容。new 对应于 delete ,new[] 对应于 delete[]
- 如果有多个构造函数,则必须以相同的方式使用 new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用 new 初始化指针,而在另一个构造函数中将指针初始化为空,这是因为 delete 可以用于空指针(但对于不是使用 new 初始化的指针使用 delete 时,结果将是不确定的,并可能是有害的)
- 应定义一个复制构造函数,通过深复制将一个对象初始化为另一个对象
- 应当定义一个赋值运算符,通过深复制将一个对象复制给另一个对象
返回对象: 如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率
- 返回对象将调用复制构造函数,而返回引用将不会
- 引用指向的对象应该在调用函数执行时存在
- 被声明为 const 的引用,返回类型必须为 const
- 如果被返回的对象是被调用函数中的局部变量,则不应该按引用方式返回它,因为在被调用函数执行完时,局部对象将调用其析构函数
指针和对象小结:
- 使用常规指针表示法来声明指向对象的指针:
String * first
- 可以将指针初始化为指向已有的对象:
String * second = &saying[0];
- 可以使用 new 来初始化指针,这将创建一个新的对象:
String * third = new String(sayings[choice]);
- 对类使用 new 将调用相应的类构造函数来初始化新创建的对象
- 可以使用
->
运算符通过指针访问类方法 - 可以对对象指针应用解除引用运算符(*)来获得对象
2. 类继承(共有继承,保护继承,私有继承)
1. 基类与派生类
① 构造函数:访问权限的考虑
派生类不能直接访问积累的私有成员,而必须通过基类方法进行访问。创建派生类对象时,程序首先创建基类对象。从概念上讲,这意味着基类对象应当在程序进入派生类构造函数之前被创建。
可以对派生类成员使用成员初始化列表语法,在这种情况下,应在列表中使用成员名,而不是类名。
② 派生类:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
- 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
- 基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员
- 派生类的构造函数总是调用一个基类构造函数,可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数
- 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用几类析构函数
③ 派生类与基类的关系: (不可以将基类对象和地址赋给派生类引用和指针)
- 派生类对象可以使用基类的方法,条件是方法不是私有的
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象
- 基类引用可以在不进行显示类型转换的情况下引用派生类对象
2. 多态公有继承: ① 在派生类中重新定义基类的方法 ② 使用虚方法(关键字 virtual
)
虚函数::
- 在基类方法中使用关键字
virtual
可使该方法在基类以及所有的派生类中是虚的 - 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不是使用为引用或指针类型定义的方法。这称为动态联编或晚期联编,这样基类指针或引用可以指向派生类对象
- 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的
编译器处理虚函数方法: 给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组被称为虚函数表 。虚函数表中存储了为类对象进行声明的虚函数地址。
使用虚函数在内存和执行速度方面的成本:
- 每个对象都将增大,增大量为存储地址的空间
- 对于每个类,编译器都创建一个虚函数地址表(数组)
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
关于虚函数的使用:
- 构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数
- 析构函数应该是虚函数,除非类不用做基类(通常给基类提供虚析构函数)
- 友元不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数
- 如果派生类没有重新定义函数,将使用该函数的基类版本;如果在派生类中重新定义函数,将隐藏同名的基类方法,不管参数特征表如何。
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化
- 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本
抽象基类: 原型中包含 =0
的虚函数是纯虚函数,而包含纯虚函数的类只能用作基类,且该纯虚函数可以被**多个派生类重新定义:virtual double Area() const = 0
3. 静态联编和动态联编
函数名联编: 将源码中的函数调用解释为执行特定的函数代码块
- 在编译过程中进行联编被称为静态联编 或 早期联编 (编译器对非虚方法使用静态联编)
- 编译器必须生成能够在程序运行时选择正确的虚方法的代码,被称为动态联编 或 晚期联编
如果要在派生类中重新定义基类的方法,则将它设置为虚方法
向上强制转换: 将派生类引用或指针转换为基类引用或指针,这使公有继承不需要进行显示类型转换
向下强制转换: 将基类指针或引用转换为派生类指针或引用。如果不使用显示类型转换,则向下转换不被允许
3. C++中的代码重用
1. 私有继承
(使用私有继承,类将获得实现)
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,这意味着基类方法将不会成为派生类对象的公有接口的一部分,但可以在派生类的成员函数中使用它们。
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
让保护派生或私有派生的基类方法在派生类外面可用:
- 定义一个使用该基类的派生类方法
- 将函数调用包装在另一个函数调用中,即使用一个
using
声明来指出派生类可以使用特定基类成员,即使采用的是私有派生
2. 多重继承
① 虚基类: 使得多个类(它们的基类相同)派生出的对象只继承一个基类对象
class Singer : virtual public Worker{...};
class Waiter : public virtual Worker{...};复制代码
- 1
- 2
Worker 被用作 Singer 和 Waiter 的虚基类
对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数
class A
{
int a;
public:
A(int n = 0) : a(n){}
...
};
class B : public A
{
int b;
public:
B(int m = 0,int n = 0) : A(n),b(m){}
...
};
class C : public B
{
int c;
public:
C(int q = 0,int m = 0,int n = 0) : B(m,n),c(q){}
...
}复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
C类构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。这里C类的构造函数使用值q,并将值 m 和 n 传递给 B 类的构造函数;而 B 类的构造函数使用值 m,并将值 n 传递给 A 类的构造函数。
- 如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显示的调用该虚基类的某个构造函数
- 如果基类是虚基类,派生类将包含基类的一个子对象,如果基类不是虚基类,派生类将包含多个子对象
- 如果类从不同的类那里继承了两个或更多的同名成员,则使用该成员时,如果没有用类名进行限定,将导致二义性
- 派生类中的名称优先于直接或间接祖先类中的相同名称
3. 类模板: template<class Tyep>
模板的具体化:
① 隐式实例化:它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:
ArrayTP<int,100> stuff; //implicit instantiation复制代码
- 1
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<double,30> *pt; //a pointer,no object needed yet
pt = new ArrayTP<double,30>; //now an object is needed复制代码
- 1
- 2
② 显示实例化:当使用关键字 template
并指出所需类型来声明类时,编译器将生成类声明的显示实例化
③ 显示具体化:是特定类型(用于替换模板中的泛型)的定义,具体化模板格式:template <> class Classname<specialized-type-name>{...}
④ 部分具体化:部分限制模板的通用性:template <class T1,class T2> class Pair{...}