图文并茂C++精华总结 复习和进阶
字面常量不可以有引用,因为这也不需要使用符号来引用了,但是字面常量却可以初始化const引用,这将生成一个只读变量;
对变量的const修饰的引用是只读属性的;
也就是说,const修饰的引用,不管是变量初始化还是字面常量初始化,总是对应一个只读变量。
#
函数能够重载和返回值无关,所以两个函数若是只有返回值不同,这是无法重载的。
#
直接调用构造函数将产生一个临时对象,其生命周期只在一条语句时间,作用域仅在一条语句内。
现代C++编译器会尽量(是看情况去尽量,有些情况不一定确保)减少临时对象的产生,例如用一个临时对象来初始化一个新对象的场景。
减少临时对象产生,也就能减少构造函数调用的次数,加大程序执行的效率。
#
类中的const成员一定不是常量。
const成员函数注意事项:const成员函数在类中的声明处,和const成员函数的定义处,的后面,
加上const修饰。
const对象只能调用const成员函数,const成员函数只能调用const成员函数,
const成员函数内不可以直接改写成员变量的值。
注意理解上面的话,也就是说,
如果const对象的某个const成员函数内嵌套调用多个子成员函数,
那这些子成员函数都必须是const修饰。
const对象,可以定义非const与const成员函数,但是只能调用const成员函数。
const对象的成员变量是只读变量,编译时作是否在赋值符号左边的检查。
const对象的成员变量可以在构造函数(例如普通有参或无参构造函数和拷贝构造函数)中被初始化。要注意初始化和赋值的不同。
言不及意,具体请看下面图片代码所要表达之内容。
#
有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;
没有 const 修饰的成员函数,对数据成员则是可读可写的。
#
运算符重载规则和函数重载一样:根据函数参数决定能否构成重载关系,与返回值无关。
#
int a = 1; int b = ++a; 这样应该是a是2 b是2 。int a = 1; int b = a++; 这样应该是a是2 b是1 。
#
引用很容易与指针混淆,它们之间有三个主要的不同:
1 不存在空引用。引用必须连接到一块合法的内存。
2 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
3 引用必须在创建时被初始化。指针可以在任何时间被初始化。
#
静态成员变量在类外分配空间,是在全局数据区分配空间,也就是说其存储区和静态局部变量,全局变量是一样的。
同时需要注意,静态成员变量也必须在类的外部进行定义。
可以通过类名访问静态公有成员变量。静态成员变量为整个类所整个类所有。
和成员函数跟类似啊,那么,可以通过类名访问公有的成员函数吗?
==》 当然不可以,见下图
自己也假设个场景思考下该问题:
通过类名访问成员函数,不带this指针。
举个例子:假设通过类名了调用了一个该成员函数,且该成员函数内访问了某成员变量,
因此种方式调用成员函数不带有this指针,故无法判定该去获取哪个对象的成员变量)。
可以通过类名访问公有静态成员函数。但
是静态成员函数不可以直接访问非静态的成员变量,因为不含this指针信息。
静态成员函数不需要实例化就可以被调用,不可以调用非静态成员(包括静态成员变量和成员函数)
反正一个意思: 不带this指针的,和带this指针的,不能沾上边就行了。
因为非静态成员函数可以访问非静态的成员变量(每个对象都有自己各自的非静态成员变量),
这即表明非静态成员函数隐含了this指针信息,
而静态成员函数不含有this指针信息,这是固定属性,
也无法通过访问非静态成员来改变其属性,使其包含this指针信息。
关于静态成员函数与非静态成员函数。
我有点理解了,编译器就是搞了这两套东西。
相辅相成,为方便用户使用而做的。
条条大路通罗马,多搞他一条路,用起来时或许能方便些。
一套带this,一套不带this。前者是私家车,后者是公交车。
私家车上会有吃私房菜(调用非静态成员变量)的需要,所以要携带this指针信息。
公交车上就吃统一的盒饭:只能调用静态成员变量或其他静态成员函数
(如果调用了其他公交车,也肯是只能吃统一的盒饭)
对静态成员函数的一点备注,结合单例模式时的使用:
plus say: 上述截图扯到了单例模式,图中代码所展示的单例模式的使用方法也可以称之为二阶构造法。
二阶构造,应该说是个人的一种编程风格吧。第一阶段不涉及系统资源的申请,
那也可以合并入第二阶段啊。
本质还是要检查系统资源申请是否有效(大多是根据返回值检查,如new malloc fopen 等),
如果申请失败则执行析构函数,释放目前已经申请到的资源,
例如之前申请资源时,执行fopen文件a成功了,但是fopen文件b失败了,
所以在此处析构内应该执行fclose文件a)。
看来还要维护一个已打开资源的数组或链表。
二阶构造方法可以结合保存已打开资源的数组或链表,使得代码更加清晰,
一眼就能判断出析构的时机,以及在析构函数内应该释放哪些资源。
#
重载赋值操作符相对其他运算符有些特殊,只能作为成员函数来重载。
运算符重载规则和函数重载一样:根据函数参数决定能否构成重载关系,与返回值无关。
C++中的重载能够扩展操作符的功能。操作符的重载以函数的方式进行,操作符重载的本质就是用特殊形式的函数扩展操作符的功能。
我们使用operator关键字来标识该特殊函数。
#
引用是指针常量,指针常量是指如 int* const p = 。。。;这样的指针,也就是说*p是可以改变的。
int *p = new int(5);
int &a =*p;
此时p指向一个无名的变量,该变量由*p表示,所以int &a =*p;
这个问题可以查找c++标准关于operator*的返回值类型的描述。 是左值,才可以初始化非const引用。
来自网友菜鸟大神的解释:
用专业术语说,表达式*p的值是个int类型的左值,所以可以初始化非const引用。而 int a,b;
表达式a+b的值是int类型的右值,所以不能初始化非const引用。
关于左值和右值,可以参考c++标准operator*的返回值类型的描述。
我的实测如下图:
#
类的成员函数可以访问该类的所有成员变量,无论是否public修饰。类的成员函数只有一套,是共用的。
#
1. 只要一个类不含有虚函数, 那么实例化对象时,可以不必实现该类内部的成员函数,只声明即可。 2. 只要一个类拥有虚函数,当定义实例化该类对象时,就必须实现其全部的虚函数定义,否则编译报错。 ( 当一个子类继承于父类时,子类内的重写虚函数(与父类内的某个虚函数:同函数名、返回值、函数参数)也是虚函数:
在实例化该子类对象时,也必须实现子类内部存在的全部虚函数定义,否则编译报错。)
#
要严格区分,只有在构造的时候,()和=可以无差别使用,都表示初始化。
但是在进行纯粹赋值的时候,例如非构造函数的其他成员函数在调用时被赋值,obj.normal_func() = 99;
此时只能用=来表示该赋值,而不能使用() .
要注意判别当前过程是构造还是赋值。
#
函数调用操作符是可重载的,且只能重载为类的成员函数。
函数调用操作符可以有多个不同的重载函数。
重载函数调用操作符的意义在于取代函数指针
(我的理解:
以前是传递函数指针,现在只需要传递对象引用即可,通过引用获取函数对象)。
有了函数对象以后,当我们看到MyClass obj; obj(99); 这样的语句们时,千万不要觉得诧异,
为啥已经构造过了,后面又调用一次obj(99) ?
那么此时单看仅有的这两条语句,应该存在两种可能性:
1. obj(99) 等价于obj=99, 这是在赋值
2.obj(99) 这是在调用obj对象的仅带一个参数的函数调用操作符的重载函数,99是传入的参数。
#
只能通过成员函数来重载数组操作符,该重载函数能且仅能使用一个参数。
数组操作符重载能够使对象模拟数组的行为。
#
如果一个类看似是空的,
实则包含了编译器默认提供的无参构造函数、拷贝构造函数、重载赋值操作符函数、析构函数。
当用户提供了自定义构造函数(自定义拷贝构造函数也算),编译器不再提供默认的无参构造函数。
#
在进行深拷贝的时候必须重载赋值操作符;
赋值操作符重载函数和拷贝构造函数具有同等的重要意义;
string类,在一块数据空间上保存字符数据,借助一个成员变量保存当前字符串长度信息;C++开发时应该避免C中的惯用思想,
例如指针使用、
直接通过char* p指向的字符串给string类对象赋值
(这种操作是不会改变string类对象内部表示字符串长度信息的成员变量的!)等。
#
智能指针一句话概念释义: 智能指针类对象在其生命周期结束后会自动释放所指向且涉及到的相关所有堆内存。
智能指针三板斧:
1. 在仅带一个参数的构造函数内对智能指针类内部的它类指针赋值,使其指向外部的它类对象。
2. 使用成员函数的方式重载指针访问->操作符(返回值为智能指针内部的它类指针) 和
*操作符(返回值为智能指针内部的它类指针指向的它类对象)。
3.在析构函数内delete它类指针(这就可以触发它类对象的析构函数被执行)。
按照上述已有的分析,我们现在或许可以写出令一个智能指针a指向一个智能指针b,再令b指向c…指来指去很多次,最后才指向实际的用户对象,这样的代码,
稍加思考,这必将成为日后bug的一个来源。
良好的编程经验要求: 一块堆内存最多只能由一个指针来标识。
这是非常重要的一点,
接下来我们继续实现的拷贝构造函数和赋值操作符重载函数就受此影响:
我们要完成指针指向内存所有权的转移(复制后,再delete原有的指针),而不是单纯复制。
#
工程中千万不要重载逗号操作符,这会背离逗号表达式从左向右计算表达式的原生意义。
自己写了重载逗号操作符函数还不如不写,
不写,也是支持对象或函数作为逗号表达式的子表达式的,
而且这种情况下反而能够按照从左向右的顺序来执行子表达式。
#
函数重载必须发生在同一个作用域。
父类中的某成员函数和子类中的同名成员函数处于不同的作用域,
他们纯粹只是同名,他们的关系不属于概念上的重载关系.
很明显,子类中的同名函数会屏蔽对父类中的同名函数的访问,这是同名覆盖的表现。
显然,函数重载和同名覆盖是对立的两种表现。virtual关键字的诞生,对原本会造成同名覆盖的函数们实现了多态调用的支持。
当然,最原始的方式,继承关系中的成员变量或成员函数的同名覆盖问题可以通过作用域分辨符来解决,达到成功访问程序员希望访问的成员。
继承关系中,通过父类指针过引用访问子类对象将退化为访问父类对象的效果:
无法访问到子类的成员。
以存在同名覆盖这种典型例子为例,
此时通过父类指针或引用访问的同名成员(变量或函数)只能是父类中的成员。
PS:此时不考虑virtual关键字,只考虑同名覆盖。
什么是多态,举例说明:
通过父类的指针或引用:
如果访问的是父类对象,访问到的就是父类对象的成员;
如果访问的是子类对象,访问到的就是子类对象的成员。
这是多态。可以通过virtual关键字来达到目的。
#
调用成员函数时,对象地址作为参数隐式传递。这就是this指针。
#
在一个类中定义一个纯虚函数,这个类就成为了抽象类,即只能用来被继承,而不能定义对象。
因为抽象类中含有纯虚函数,纯虚函数也是虚函数,所以支持多态,其子类可以定义同名函数。 对抽象类的访问:
我们可以通过抽象类的指针来访问,
这样得到的将是对继承于抽象类的各个子类的实体对象的访问,这是对多态的实际运用。
继承于抽象类的子类必须实现抽象类内的所有纯虚函数,
如果有遗漏实现,即只实现了部分,那么子类将成为抽象类。
打类比: 抽象类的特点就像新冠病毒传递,
子类内若实现了抽象类内的所有纯虚函数,就好比杀死了所有的新冠病毒,
子类自己才不会被感染而成为抽象类。
接口是一种特殊的抽象类,
即没有成员变量,只有成员函数,并且所有的成员函数都是公有的纯虚函数的类。
#
构造函数内会负责虚函数调用的前期工作,
构造函数结束之前并不能保证虚函数表指针被正确初始化,所以构造函数不能成为虚函数。
而析构函数可以成为虚函数,同时,建议在设计类时将析构函数声明为虚函数。
父类的析构函数成为虚函数,将具备多态的特性。只要一个类被声明为父类,其析构函数都应该是虚函数。
这具有非常实际的意义:当各个子类对象或父类对象进行构造时申请了系统资源,
那么通过多态的特性,当指向具体各个子类对象或父类对象的父类指针
(这个是编译器不确定的,运行时才确定)被析构时,就可以做到正确调用具体对象的析构函数,正确地释放已申请的系统资源。
也就是说,虚析构函数相比普通的析构函数,具备多态性。
现在讨论第二个点:
构造函数内部可以发生继承关系中的多态行为( 即调用virtual修饰的其他成员函数 )吗,
析构函数内部可以发生继承关系中的多态行为吗?
都不能。 因为:在构造函数执行时,虚函数表指针可能未被正确初始化。
在析构函数执行时,虚函数表指针有可能已经被摧毁。
即使在构造函数或析构函数内部调用virtual修饰的其他成员函数,
也都只能是当前类中定义的函数版本。
#
dynamic_cast要求在相关的类中必须有虚函数。
子类指针不可以指向父类对象。
子类指针也不可指向父类对象的指针,
即使使用dynamic_cast将父类对象的指针赋值给子类指针,
代码运行时候得到的dynamic_cast的返回值,即子类指针,也将是空指针,即0值。
dynamic:动态, 所以dynamic_cast包含在代码运行时才得到转换结果是否成功的涵义。
#
catch遵循极为严格的类型匹配,
示例1: char short double 类型均无法捕获到throw出的常量1.因为常量1默认是int类型。
示例2: throw出的字符串常量也无法被char* 或 string 捕获到,因为字符串常量是const char *类型。
catch(...)一般不能放在代码中的首个捕获处,否则会编译报错。
对象的异常throw后只能被catch有且仅有一次。
异常中的赋值兼容原则: 子类的异常对象可以被父类catch到。
构造和析构函数内不要抛出异常。
#
函数参数从右向左给默认值,函数调用时实参按照函数参数列表内从左向右的顺序匹配。
#
类中的静态成员(函数或变量),以及类中的枚举值,都可以通过类名的形式调用(通过类名调用就是使用类名+作用域分辨符的方式)。
类中的枚举值,因为是常量,存储在代码段,和this指针无关,所以也可以通过类名调用。
类中的枚举值常量,可以被类名直接访问。
类中的枚举变量,不可以被类名直接访问。
#
类中成员的访问级别是编译器检查的,运行时可以通过指针访问而不受访问级别的限制。
#
sizeof一个函数,知道函数的返回值就行,编译器即可确定所求的大小,并且不需要调用函数执行。
#
函数重载,调用时的匹配,示例
#include <iostream> using namespace std; void func(int para){ cout << "void func(int para)" << endl; } void func(int& para){ cout << "void func(int& para)" << endl; } void func_1(int& ){ cout << "void func_1(int&)" << endl; } int main(){ func(3); // 匹配的是void func(int para),因为3是字面常量,所以不可以有引用,因此不可能匹配参数是引用的函数。 int a = 3; //func(a); 编译报错,无法匹配。 因为存在多个可匹配的函数,因此无法匹配。 int&b = a; //func(b); 编译报错,无法匹配。 因为存在多个可匹配的函数,因此无法匹配。 return 0; }
#
#
#-----------------------------------------------------------
多种初始化方式: int a(5) ;、 int a=5; 、 int a{5}; 、int a[]{1,2,3};、int a[] ={1,2,3};
可以看出使用大括号{}符号初始化数组元素,会依次调用构造函数。
使用大括号{}符号初始化的优势是可以避免隐式转换
(待增补实验:用类的对象,子类对象初始化父类对象,来进一步验证该结论)。 ?
避免隐式转换的好处在于有时能通过编译器报错来使程序员能更全面地掌控程序。
#
#-----------------------------------------------------------
序列式容器,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string 等)或使用结构体自定义类型的元素。 关联式容器则大不一样,此类容器在存储元素值的同时,还会为各元素额外再配备一个值(又称为“键”,其本质也是一个 C++ 基础数据类型或自定义类型的元素),
它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。
也就是说,使用关联式容器存储的元素,都是一个一个的“键值对”( <key,value> ),这是和序列式容器最大的不同。
除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。 C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset。
.
/************* 社会的有色眼光是:博士生、研究生、本科生、车间工人; 重点大学高材生、普通院校、二流院校、野鸡大学; 年薪百万、五十万、五万; 这些都只是帽子,可以失败千百次,但我和社会都觉得,人只要成功一次,就能换一顶帽子,只是社会看不见你之前的失败的帽子。 当然,换帽子决不是最终目的,走好自己的路就行。 杭州.大话西游 *******/