基于《Thinking in C++》
CPP复习笔记
1. 静态成员变量
- 静态成员变量用static修饰,它只属于类而不是对象,因此所有这个类的对象享用同一个静态变量值。
静态成员变量必须要在类外初始化
type class::name = value;
初始化的时候不用带static但需要有类型,protected,privated,public都可以被这样初始化
静态成员变量的调用
//通过类类访问 static 成员变量 Student::m_total = 10; //通过对象来访问 static 成员变量 Student stu("小明", 15, 92.5f); stu.m_total = 20; //通过对象指针来访问 static 成员变量 Student *pstu = new Student("李华", 16, 96); pstu -> m_total = 20;
- static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
- 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
- static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
- 静态成员变量初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。
- 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
- 静态成员变量可以成为派生类和基类共同使用的数值,也可以成为成员函数的可选参数。
- 静态成员变量可以是所属类的类型,普通数据成员只能成为该类的指针或引用。
2. 静态成员函数
静态成员函数的地址可以用普通函数指针调用
class A{ public: static fun(){}; int fun1(){}; } int (*pf1)()=&base::fun; int (base::*pf2)()=&case::fun1;
- 静态成员函数不可以调用类的非静态成员,因为这个静态成员函数并不带有this指针。
- 静态成员函数不可以同时声明为 virtual const volatile函数。
- 静态成员函数不需要对象名即可调用。
- 非静态成员函数可以自由调用静态成员函数和静态成员变量。
3. 引用和地址
- 引用类似于某个变量的别名,完全享用同一片地址。
- 引用必须在定义的时候初始化。
- 引用对象已经初始化不能重新引用另外的变量。
4. 拷贝构造函数
- CExample(const CExample& C)
就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。 调用时机
- 类作为一个参数整体传入一个函数的时候,需要调用这个类的拷贝构造函数,进行形参和实参的复制
- 类作为一个结果返回的时候,先产生一个临时变量,调用拷贝构造函数将返回值拷贝到临时变量,析构返回的变量,再析构临时变量
需要通过另一个变量初始化的时候
class mode{...} mode A(10); mode B = A;
- 拷贝构造函数分为浅拷贝和深拷贝。
默认拷贝构造函数是浅拷贝的一种
- 默认拷贝构造函数无法处理静态成员变量只是简单复制
- 需要自己写浅拷贝构造函数进行静态成员变量的复制
- 如果被拷贝对象中包含指针,进行逐位拷贝后新旧两个指针将指向同一个空间,并且将被重复释放
深拷贝用于需要动态创建新空间时
Rect(const Rect& r) { width = r.width; height = r.height; p = new int; // 为新对象重新动态分配空间 *p = *(r.p); }
- 可以创建一个private的拷贝构造函数声明来解决默认值拷贝。???
5. const
- 修饰指针变量时:
- 只有一个const,如果const位于*左侧,表示指针所指数据是常量,不能通过解引用修改该数据;指针本身是变量,可以指向其他的内存单元。
- 只有一个const,如果const位于*右侧,表示指针本身是常量,不能指向其他内存地址;指针所指的数据可以通过解引用修改。
- 两个const,*左右各一个,表示指针和指针所指数据都不能修改。
- 修饰函数参数时:
- 不能改变该参数的值。
- 修饰函数时:
- 该函数不能改变调用的参数值,同样地,该函数也不能调用任何非const函数。
- 修饰返回值时:
- 指针返回时:只能赋值给同样用const修饰的左值
- 值传递时:const并没有什么意义
6. 对象初始化和析构
空初始化:即无参数无括号形式
- 如int i,new int,new int[10].当在所有函数之外时,初始化为0;当在某一函数中时,没初始化。
值初始化:即无参数有括号形式,且括号只能在类型名后,而不能在变量名之后,即只能创无名对象,对象被值初始化为0.
- 如:int() //创建了一个无名对象,其被值初始化为0.一般将该无名对象初始化化或赋值给某有名对象,或直接作为无名对象使用
显式初始化:即有参数有括号形式,且当为有名对象时括号在对象名之后,为无名对象时括号在类类型名之后。
- 如:int i(5);
- new int(5);
- 如:int i(5);
- 以下四种必须使用初始化列表:
- 初始化一个引用成员变量
- 初始化一个const变量
- 当我们在初始化一个子类对象的时候,而这个子类对象的父类有一个显示的带有参数的构造函数
- 当调用一个类类型成员的构造函数,而它拥有一组参数的时候
- 析构函数通常使用默认析构函数,但是在之前进行空间改变(指针移位等)的时候一定要自己写析构函数。
析构数组或类组:
class A { A(){m_a=new int[10];} ~A(){delete [] m_a;} int * m_a; }
强制类型转换支持但并不推荐,推荐使用以下较温和的方法:
pd = static_cast<double*>(pv);
- 初始化列表不管怎么写,初始化的顺序也只是按照原类内声明的顺序进行。
7. 重载函数和默认参数
- 重载函数的调用匹配
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
- 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
- 使用标准转换匹配:如int 到double、double到int、double到long double、Derived*到Base*、T*到void*、int到unsigned int;
- 使用用户自定义匹配;
- 使用省略号匹配:类似printf中省略号参数
- 同一作用域中有相同函数名但是有不同参数列表的可见函数构成重载关系。
- 内层作用域的函数会隐藏外层的同名函数,同样的派生类的成员函数会隐藏基类的同名函数。
- 如果要在函数内部调用重名的全局变量则要以“:: va”这样的形式调用。
- 在编译器中,编译器看到的函数名为“类型+名称+从左往右的参数列表”,但事实上在调用重载函数时,仅仅有返回类型不同是不能成立的,因为编译器无法判断你调用的是哪个函数,具有二义性。
8. 运算符重载
两种重载方式的比较:
一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。 C++提供4个类型转换函数:reinterpret_cast(在编译期间实现转换)、const_cast(在编译期间实现转换)、stactic_cast(在编译期间实现转换)、dynamic_cast(在运行期间实现转换,并可以返回转换成功与否的标志)。
若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数。
当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部 类型的对象,该运算符函数必须作为一个友元函数来实现。当需要重载运算符具有可交换性时,选择重载为友元函数。
注意事项:
除了类属关系运算符”.“、成员指针运算符”.*“、作用域运算符”::“、sizeof运算符和三目运算符”?:“以外,C++中的所有运算符都可以重载。
重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
运算符重载不能改变该运算符用于内部类型对象的含义。它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。
运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符。
9. 继承和组合
- kind of关系下用继承,part of关系下用组合。
继承
class Human { … }; class Man : public Human { … }; class Boy : public Man { … };
组合
class Eye { public: void Look(void); }; class Nose { public: void Smell(void); }; class Mouth { public: void Eat(void); }; class Ear { public: void Listen(void); }; class Head { public: void Look(void) { m_eye.Look(); } void Smell(void) { m_nose.Smell(); } void Eat(void) { m_mouth.Eat(); } void Listen(void) { m_ear.Listen(); } private: Eye m_eye; Nose m_nose; Mouth m_mouth; Ear m_ear; };
- 继承的关系不同对这个派生类并无影响,而是对该派生类的派生类产生影响。例如private Base(10),则对于该派生类的派生类来说,Base不可见。
10. inline & extern
- 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
- 定义在类声明之中的成员函数将自动地成为内联函数。
- 宏替换是单纯地代码替换,inline函数真正具有函数的特征。
- extern 表示该声明已经定义在别的文件中了。
11. virtual函数与纯虚函数
- 虚函数:类Base中加了Virtual关键字的函数就是虚拟函数(例如函数print),于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的指针point指向派生类Derived的对象时,对point的print函数的调用实际上是调用了Derived的print函数而不是Base的print函数。
- 我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。也就是说,virtual函数被继承后可以自动动态绑定当前对象。
纯虚函数:只声明,无定义,包含纯虚函数的类称为抽象类,无实际作用,只作为基类。
class <类名> { virtual <类型><函数名>(<参数表>)=0; … };
重载和覆盖的区别
- 重载的几个函数必须在同一个类中;
覆盖的函数必须在有继承关系的不同的类中 - 覆盖的几个函数必须函数名、参数、返回值都相同;
重载的函数必须函数名相同,参数不同。 - 覆盖的函数前必须加关键字Virtual;
重载和Virtual没有任何瓜葛,加不加都不影响重载的运作。
- 重载的几个函数必须在同一个类中;
关于C++的隐藏规则:
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。 - 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
重写 重载 重定义
重写(override):
父类与子类之间的多态性。子类重新定义父类中有相同名称和参数的虚函数。1) 被重写的函数不能是 static 的。必须是 virtual 的 ( 即函数在最原始的基类中被声明为 virtual ) 。
2) 重写函数必须有相同的类型,名称和参数列表 (即相同的函数原型)
3) 重写函数的访问修饰符可以不同。尽管 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的
重载 (overload):
指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。重定义 (redefining):
子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。
重写与重载的区别 (override) PK (overload)
方法的重写是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关 系,是水平关系。
重写要求参数列表相同;重载要求参数列表不同。
重写关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
12. binding
- 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
D* pD = new D(); //pD的静态类型是它声明的类型D*,动态类型也是D* B* pB = pD; //pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D* C* pC = new C(); pB = pC; //pB的动态类型是可以更改的,现在它的动态类型是C*
- 静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
- 动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
- 只有虚函数是动态绑定,其余函数都是静态绑定。动态绑定的函数调用的函数体看实际上的对象类型,静态绑定的函数调用的函数体看声明的对象类型。
- 虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。
13. upcasting、downcasting与类指针
- 将基类引用转换为派生类引用称为upcasting,因为在继承图上式上升的。
对于一个使用了虚函数的基类来说:
Base b = d;//直接赋值(产生切割) b.Test(); Base& b2 = d;//使用引用赋值(不产生切割) b2.Test(); Base* b3 = &d;//使用指针赋值(不产生切割) b3->Test(); //覆盖方法和子类数据丢失的现象生成切割(slice)
14. 模板
模板的一般形式:
Template <class或者也可以用typename T> 返回类型 函数名(形参表) {//函数定义体 } //template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型. template <class T> inline T square(T x) { T result; result = x * x; return result; };
15. 异常探查
http://www.cnblogs.com/ggjucheng/archive/2011/12/18/2292089.html
16. explicit
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
explicit可以避免隐式调用构造函数。
17. 友元函数
class A{
friend int print(); //友元函数不可被继承
}
int print(){}; //可以定义在类内或者类外
int main{
A obj;
print(); //可以直接调用友元函数
}