C++的语法 学习笔记1
C++的语法 学习笔记1
继承与多态
继承的基本概念 |
继承是在现有类的基础上进行扩展; 继承是在现有类的基础上,增加了属于自己的类成员; 继承之后的类拥有之前类的成员; 继承之后的类也拥有自己的类成员; 父类、基类、超类:被继承的类; 子类、派生类:继承之后的类; 继承方式分为:公有继承、私有继承、保护继承; 继承方式的访问符为:public、private、protect; |
||||||||||||||||||
继承的语法 |
C++使用:冒号表示继承; 继承的访问控制符可以省略; 如果派生的是class,则默认私有继承;class A:B{}; 如果派生的是struct,则默认公有继承;struct A:B{}; 父类必须是已经定义好的类,只声明而未定义的类,不能作为父类;
|
||||||||||||||||||
子类成员 |
子类成员分类:从父类继承的成员、自己定义的成员; 子类可以像访问自己的成员一样访问父类成员; 但是,父类不能访问子类的成员,因为父类不知道子类有哪些成员; 私有成员无法继承,私有成员只属于父类,而不属于子类; |
||||||||||||||||||
继承成员的访问级别 |
public继承:父类成员访问级别不变 private继承:public→private,protect→private protect继承:public→protect 私有继承只能进行一级继承,继承后所有成员变为私有,无法再继承下去; 保护继承可以进行多级继承,代代相传; protect只有在继承时,才有实质性的用处; protect在没有继承的类中,根本无用,只用public和private就够了; |
||||||||||||||||||
恢复访问级别 |
使用using可以恢复继承成员的访问级别; 恢复后的访问级别只能到父类的级别,而无法超越; class A{protected:int a;void f();};class B:private A{protected:using A::a;using A::f;}; 使用父类类名和作用域解析运算符::; class A{protected:int a;void f();};class B:private A{protected:A::a;A::f;}; |
||||||||||||||||||
静态成员 友元 |
静态成员始终只有一个,无论继承多少次; 静态成员不能在子类中进行初始化; 友元不会在继承中传递; 父类是不是友元和子类是不是友元没有任何关系; |
||||||||||||||||||
不会被子类继承的 |
构造函数; 复制构造函数/拷贝构造函数; 析构函数; 赋值操作符函数; |
||||||||||||||||||
|
子类的构造函数不能初始化父类成员; class A{public:int m;}; class B:public:A{B():a(3){}};这是错误的; B中第一个:表示继承,第二个:表示访问权限,第三个:表示初始化列表; 子类构造函数只能初始化子类成员; 父类成员需要使用父类构造函数进行初始化; 构造函数内的语句是赋值,而不是初始化; 初始化列表才是真正的初始化; 父类的构造函数是由子类的构造函数进行调用的; 先用子类的构造函数调用父类的构造函数,初始化父类成员; 再用子类的构造函数初始化子类成员; 先构造父类→再构造子类;析构顺序与此相反;
|
||||||||||||||||||
子类构造调用父类构造 |
显式调用:通过构造函数的初始化列表的方式显式调用父类的构造函数; class A{public:A(int i)};class B:public A{public:int b;B(int j):A(j),b(j){}}; 隐式调用:如果子类的初始化列表中没有显式调用父类的构造函数,则会隐式调用父类的默认构造函数; 隐式调用的前提是父类要有默认构造函数,否则会报错; class A{public:A(int i)};class B{public:B(){}};错误,A没有默认构造函数,无法隐式调用; 如果父类没有默认构造函数,则子类必须在初始化列表显示调用父类的构造函数; class A{public:A(int i)};class B{public:B():A(3){}B(3){}}; B():A(3){}正确,B(3){}错误,因为A没有默认构造函数; 子类的构造函数类内声明,类外定义时,不能在声明时调用父类构造函数; 因为,构造函数声明时不能使用初始化列表; class A{};class B:public A{public:B();B(int i):A();};B::B(int i):A(){} 其中,在声明中使用初始化列表调用A的构造函数是错误的,而在定义时使用是正确的; 子类只能初始化它的直接父类,而不能初始化父类的父类; class A{};class B:public A{};class C:public B{public:C():A(){}}; C在初始化列表中调用A的构造函数是错误的,因为A不是C的直接父类;
|
||||||||||||||||||
初始化顺序 |
在继承时,不管父类的构造函数出现在初始化列表的什么位置,或者没有显式调用父类构造函数,父类都会先于子类成员进行初始化; 如果是多重继承,则按照继承顺序依次初始化父类; 父类初始化完成后,会按照子类成员声明顺序(而不是在初始化列表中的位置),依次初始化; class A{};class B:public A{public:int m,n;B():n(1),A(),m(3){}}; 初始化顺序为父类构造A()→子类成员(先声明的先初始化m(3),后声明的后初始化n(1)); 当子类含有父类的类对象成员时,该成员当做普通成员进行初始化; 依然是先初始化父类,然后按照声明顺序初始化各个子类成员;
|
||||||||||||||||||
复制构造函数 赋值操作符函数 |
父类的复制构造函数,复制父类部分; 子类的复制构造函数,复制子类部分; 父类的赋值操作符函数,对父类对象进行赋值; 子类的赋值操作符函数,对子类对象进行赋值;
子类默认复制构造函数→隐式调用→父类默认复制构造函数;✔ 子类默认复制构造函数→隐式调用→父类自定义复制构造函数;✔ 子类自定义复制构造函数→显示调用→父类的默认复制构造函数;✔ 子类自定义复制构造函数→显示调用→父类的自定义复制构造函数;✔ 子类自定义复制构造函数→隐式调用→父类的默认构造函数;✖错误
子类默认赋值操作符函数→隐式调用→父类默认赋值操作符函数;✔ 子类默认赋值操作符函数→隐式调用→父类自定义赋值操作符函数;✔ 子类自定义赋值操作符函数→显示调用→父类的默认赋值操作符函数;✔ 子类自定义赋值操作符函数→显示调用→父类的自定义赋值操作符函数;✔ 子类自定义赋值操作符函数→隐式调用→父类部分无法赋值;✖错误
|
||||||||||||||||||
父类和子类的转换 |
父类指针可以指向子类对象;无需显示强制类型转换; 父类引用可以指向子类对象;无需显示强制类型转换; 子类对象拥有完整的父类副本;
子类指针可以自动转换为父类指针;此时指向子类中的父类部分; 子类引用可以自动转换为父类引用;此时指向子类中的父类部分;
转换后的指针无法再指向子类特有成员; 转换后的引用无法再指向子类特有成员; 不存在从父类到子类的转换(指针、引用),因为父类不知道子类有什么成员; 转换只存在于对象的指针和引用之间,而不存在于类对象之间; 这种转换相当于将子类中子类特有部分切除,而仅仅保留了父类部分; |
||||||||||||||||||
子类转父类 转换条件 |
能否进行子类到父类的自动转换(指针、引用),关键看父类的公有成员能否访问; 公有继承:可以进行自动转换; 非公有继承:类外不可以进行自动转换; 子类内:始终可以进行自动转换,而与继承方式无关; 子类的友元函数内:始终可以进行自动转换,而与继承方式无关; 子类保护继承:则该子类的子类内,都可以进行自动转换; 私有继承:不可以在类外进行自动转换;✖
|
||||||||||||||||||
继承下的名称解析、名称隐藏、函数重载 |
子类作用域和父类作用域是两个作用域; 子类作用域嵌套在父类的作用域内; 如果在子类中找不到某个名称,就到父类中去找; 在子类中,子类的作用域优先级高于父类,也就是先找子类,再找父类; 子类成员会隐藏掉父类的同名成员(子类找到,就不去父类找了);
子类和父类的同名函数不会构成函数重载; 子类的函数会隐藏掉父类中的所有同名函数; class A{public:void f(int){} void f(int,int){}};class B:public A{public:void f(){}}; 子类中的f()不会和父类中的f(int)、f(int,int)构成重载; 子类中的f()会隐藏掉父类中的所有f函数; 函数重载的前提是,函数在相同的作用域内;(带类类型形参的函数除外) 父类和子类函数分属于两个作用域,因此无法构成重载; 子类无法继承父类的私有成员,但是父类的私有成员依然存在,在名称查找时依然可见; class A{private:int a;}; class B:public A{public:void f(){a=3;}}; 这里的a不会出现未定义的标识符错误,而是出现无法访问的错误; 未定义标识符 "a";成员 "A::a" (已声明 所在行数:7) 不可访问;
|
||||||||||||||||||
访问父类隐藏成员 |
1. 使用作用域解析运算符:: b.A::a=3; b.A::f(); 2. 使用using将子类名称引用子类中;using A::a; using A::f;using引入后的成员,将和子类成员在同一个作用域内,如果有同名函数,可以构成函数重载; 3. 类名+作用域解析运算符::A::a;A::f;
|
||||||||||||||||||
using注意事项 |
using引入的父类成员变量不能在子类中重新定义; using引入的父类成员函数可以在子类中重新定义; 重定义后的函数,可以覆盖掉从父类引入函数的某个版本; using后又被覆盖的父类函数,只能通过作用域解析运算符进行调用; |
||||||||||||||||||
继承下的重载解析 |
最佳匹配: 子类到父类的转换是标准转换(对象、指针、引用); 离父类越近的转换越好,多重继承也是如此; 不存在父类到子类的转换(对象、指针、引用); 子类指针到父类指针的转换,比到void指针的转换更好; 标准转换比自定义转换更好;
|
||||||||||||||||||
多重继承 |
一个子类从多个父类继承; 多个父类之间用逗号,隔开; 每个父类之前应指定访问级别; 如不指定访问级别,class子类默认private,struct子类默认public; class A:public B,private C{};公共继承B,私有继承C; class A:public B,C{};公共继承B,私有继承C; struct A:B,private C{};公共继承B,私有继承C; 父类的初始化顺序按照继承的顺序; 子类的初始化列表不能控制初始化顺序; 初始化顺序与子类的初始化列表顺序无关; 父类的构造函数总是先被调用,无论其是否出现在初始化列表中; 然后,才是子类成员的初始化; 1父类初始化:按照继承顺序; 2子类成员初始化:按照声明顺序; class A{};class B{};class C:public B{}; class D:public C,public A{public:int d;D():d(8),A(){}}; D先继承C,因此先初始化C,而C继承B,因此先初始化B,所以B→C; D又继承了A,C初始化完成后,开始初始化A,等两个直接父类AC都初始化完成后,开始初始化本类D; B(C的父类)→C(第一个父类)→A(第二个父类)→D(本类);
|
||||||||||||||||||
多级继承 |
被继承的父类本身也是一个子类; class A{};class B:public A{};class C:public B{};A→B→C |
||||||||||||||||||
多重继承 名称查找 |
父类中的名称查找是并行的,与父类的继承顺序无关; 若多个父类中出现了重名,则会出现二义性错误; 使用作用域解析运算符::,可以解决二义性问题;
不同父类继承的函数不能构成重载,因为他们在不同的作用域; 不同父类中有重名函数(形参名称无关紧要),会产生二义性错误; 若想要与父类函数进行重载,可以使用::或者using或者类名::,三种方式; 函数形参分别是各个父类,那么这些函数将产生二义性错误; class A{};class B{};class C:public A,public B{};void f(A& a){}void f(B& b){} 执行函数C c;f(c);执行这个函数时,编译器将无法确定将子类C转换为哪个父类(A还是B ); void f(A& a){}void f(B& b){}也就是说,将子类对象作为实参时,无法确定调用哪个函数; |
||||||||||||||||||
共同父类 虚基类 |
若两个父类AB同时继承自同一个父类C,子类中会出现多个C的副本; C的多个副本容易导致二义性错误,解决方法有2: ① 使用作用域解析运算符明确调用; ② 使用虚基类; class D{public:void f(){}};class B:public D{};class C:public D{};class A:public B,public C{}; A a;a.f();对函数f的调用存在二义性错误,因为子类A同时拥有D的两个副本,也就是有两个f; 其中一个由B继承来,另一个由C继承来; 可以使用作用域解析运算符明确调用,a::B.f();a.C::f(); a.D::f();是错误的,使用共同父类加作用域解析运算符无法解决二义性问题,因为有两个D;
|
||||||||||||||||||
虚基类 虚继承 |
在继承时使用virtual关键字,这种继承称为虚继承; virtual关键字和访问控制符先后顺序无关紧要; public virtual 和virtual public是等价的; 虚继承的基类称为虚基类; 虚基类并不是类声明时确定的,而是在继承时才确定的; 一个类本身并不是虚基类,只有他被别的类以virtu方式继承时,才成为虚基类; class A:public virtual B{};B是A的虚基类,A的虚基类是B;A虚继承B;
虚基类只有一个副本,可以避免二义性错误; 所有直接子类都应以虚继承的方式继承共同父类,否则仍然会出现多个副本;
虚基类对性能影响很大,所以很少使用; 虚基类允许简介子类对其进行初始化; 当简介子类对虚基类进行初始化时,直接子类对虚基类的初始化会被忽略掉; 虚基类的初始化是由最终子类完成的; 当所有直接子类都是虚继承时: 最终子类的构造函数通过初始化列表显示调用虚基类的构造函数,完成虚基类的初始化; 如果虚基类构造函数没有出现在最终子类的初始化列表中,则使用虚基类的默认构造; 如果虚基类的构造没有出现在最终子类的初始化列表中,且虚基类没有默认构造,则出错; 多级继承时,中间类对虚基类的初始化将被忽略; 在继承树中,虚基类的初始化总是在非虚基类之前;
虚基类→基类→成员 |
||||||||||||||||||
多态 |
定义:一种事物具有多种形态;
A*pa;pa->f();pa就是动态类型; 编译阶段无法确定pa所指向的对象类型; pa既可以指向A类型,也可以指向A的任意子类类型; A*pa=&b;pa的静态类型是A,动态类型是b; 动态类型可以改变(只需指向另一个对象即可),而静态类型无法更改; pa的类型需要在运行阶段才能确定,在编译阶段是无法确定的;
指针、引用的动态类型可以和静态类型不同; 类对象的静态类型必须和动态类型总是相同的; 把子类对象赋值给父类对象时,会把子类的特有部分切除,而把剩余的父类部分赋值给父类对对象;
|
||||||||||||||||||
绑定 |
绑定:确定调用的是哪个对象的行为; 绑定:把函数和某个类对象绑定在一起的行为;
动态绑定的结果就是,根据多绑定的动态类型,调用动态类型的函数; class A{public:void f(){}};class B:public A{public:void f(){}}; B b;A *pa;pa->f();pa=&b;pa->f(); pa调用的函数f究竟是A中的f,还是B中的f,取决于pa是什么类型; pa静态类型A,则调用A中的f; pa=&b则pa指向B类型,则调用B中的f; |
||||||||||||||||||
实现多态的条件 |
C++多态:用一个公有的父类指针(或引用),寻址出一个子类对象; C++多态:使用父类指针(或引用)访问子类中的成员函数; 非多态情况下,用一个父类指针指向子类对象,该指针只能访问子类中的父类部分,而无法访问子类中特有的成员; 多态就是为了实现:用父类指针访问子类对象中的特有成员,而不是父类成员; C++使用动态绑定实现多态; 动态绑定要使用虚函数和父类指针(或引用),二者缺一不可; 只有虚函数才是动态绑定,虚函数是实现多态的必要条件之一,非虚函数都是静态绑定; 只有指针(或引用)的动态类型可以和静态类型不同;对象类型的动态类型和静态类型始终相同; 为什么要用父类指针(或引用)? 子类可以转换为父类,父类无法转换为子类; class A { public:void f() { } }; class B :public A { public:void f() { } }; A a; B b; a = b;//正确 b = a;//错误:没有与这些操作数匹配的 "=" 运算符 子类对象绑定到子类类型;✖无意义 父类对象绑定到父类类型;✖无意义 父类对象绑定的子类类型;✖错误 子类对象绑定到父类类型;✔有意义 a=b;a.f();类对象使用的是静态绑定而不是动态绑定; 即使将b赋值给a,a依然调用自己的f,而不会调用子类b的f;
|
||||||||||||||||||
虚函数 |
子类必须公有继承拥有虚函数的基类,才能实现多态; 因为只有公有继承的子类指针才可以转换为父类指针;(引用亦是如此) 私有继承的情况下,无法将子类指针转换为父类指针;(引用亦是如此) 多态类:包含虚函数的类,称为多态类; 虚函数声明:在声明成员函数时,加上virtual关键字,那么该函数就是虚函数; 虚函数声明规则: ✔member func:只有类成员函数才可以声明为虚函数,类外函数不能声明为虚函数; ✖friend:友元函数不能是虚函数,因为友元函数不是类成员; ✖static:虚函数不能是静态函数; ✖member var:成员变量不能是虚的;virtual不能用在成员变量之前; ✔destructor:析构函数可以使虚拟的; ✖constructor:构造函数不能是虚拟的; 多个重载函数中,谁有virtual谁就是虚函数,没有virtual的就不是虚函数; 虚函数,无论经过多少次继承,始终是虚函数;虚函数代代相传; 赋值操作符函数不应定义为虚拟的,因为形参类型不同;
引入虚函数的类必须定义虚函数,或者将虚函数声明为纯虚函数; class A{public:virtual void f();};✖错误:只声明不定义; 纯虚函数:class A{public:virtual void f()=0;}; 类内声明用virtual,类外定义时不能再用virtual; class A{public:virtual void f();};virtual void A::f(){}
|
||||||||||||||||||
虚函数重定义 |
子类可以重定义父类的虚函数; 如果子类没有重定义父类的虚函数,则继承之; 子类重定义虚函数时,不能只声明而不定义; 因为虚函数必须定义,或者声明为纯虚函数; 子类中重定义虚函数时,可以省略virtual关键字; 重定义的函数声明必须和父类完全相同,否则不是重定义;返回类型、存储类型、限定修饰符; ■返回值不同:如果重定义的函数只有返回值不同,会出错; 如果定义了一个父类虚函数的重载版本,这个函数不是虚函数; 并且子类的这个重载函数会隐藏父类的虚函数; 子类的重载版本并不会和父类虚函数构成重载; ■修饰词不同:如果子类重定义的函数和父类虚函数只差一个关键词static、const或者形参有无const,则不是重定义,并且会隐藏父类虚函数;void f() const{} 返回值int和const int不是对虚函数的重定义,而是一种错误;
■特例:可以认定为重定义(虽然返回值类型不同)返回类型协变 虚函数返回某个类型A的指针或引用; 重定义返回A的子类的指针或引用;(该子类类型必须公有继承A)
|
||||||||||||||||||
虚函数调用 |
非虚函数:父类指针始终调用父类的函数; 虚函数:父类指针实际指向的对象是什么类型,就调用什么类型的函数;而不一定是父类的函数; 父类对象调用函数,始终是父类的函数,无论这个 函数虚不虚; 在通过父类指针或引用调用虚函数时,才会表现出虚函数的特性; 其他情况下调用,虚函数就像普通函数一样; 分层调用:如果子类没有重定义虚函数,则按照继承顺序逆流而上,寻找最近的重定义的虚函数进行调用; 类名调用:会打破虚函数的动态绑定规则,会破坏虚函数的多态性; 父类对象调用虚函数只能是本类中的虚函数,而不是子类中的虚函数; 因为这时子类还没有被构造出来,还没有子类的重定义函数; |
||||||||||||||||||
虚函数静态调用 |
子类的重写虚函数调用父类的虚函数; 子类虚函数调用父类纯虚函数; 父类的非虚函数调用父类的虚函数;this指针也能实现多态; 在构造函数或者析构函数中调用虚函数;
virtual void print() = 0;//纯虚函数,可以有函数体,派生类必须重写它 |
||||||||||||||||||
虚函数默认实参
|
虚函数是动态绑定的,但是虚函数的形参是静态绑定的; 父类指针调用时:如果父类虚函数中的形参有默认值,则子类中指定的默认值都不起作用,而只能使用父类中的默认值; 子类指针调用时,会使用子类中指定的形参默认值;
|
||||||||||||||||||
虚函数名称解析 |
首先确定调用该函数的对象的静态类型; 然后在该静态类型中查找函数名; 如果没找着,就去直接父类找,一层层往上找; 如果找到就会进行类型检查,如果找不到就报错; 当调用合法时,再看是不是虚函数,是不是通过指针或引用进行调用; 如果满足多态的2个条件,则根据对象的动态类型调用; 如果不满足多态条件,就直接调用该函数; 虚函数调用流程: 先从动态类型开始查找; 然后按照继承关系一级一级向上查找; 一直找到虚函数的重定义版本或者静态类型中的虚函数版本;
|
||||||||||||||||||
纯虚函数 |
声明格式:virtual void f()=0;class A{public:virtual void f()=0;}; 后面的=0只是为了告诉编译器,这个函数是纯虚函数; 纯虚函数可以在引入该函数的类中只声明,不定义; 抽象类:如果类中有纯虚函数,那么这个类就是抽象类,也叫做抽象基类; 抽象类一般作为其他类的公共基类,不能创建对象; 子类把纯虚函数重定义为虚函数后,这个函数就不再是纯虚函数,后继的子类中,这个函数都不再是纯虚函数; 引入纯虚函数的类中对其进行定义,那么就是一个完整的函数; 有定义的纯虚函数可以被调用(只能静态调用),但是仍然不能创建对象; 纯虚函数可以在类外定义,不再使用virtual和=0; 如果子类没有对父类的所有纯虚函数进行重定义,那么未被重定义的纯虚函数会被继承,该子类也是抽象类; 子类可以不重定义纯虚函数,此时,子类变成抽象类,子类不能创建对象; |
||||||||||||||||||
|
|
||||||||||||||||||
虚析构函数 |
class A{int m;};class B:public A{int n;};B b;A*pa=&b; 静态析构:如果delete pa,则只会释放B中的父类部分,而子类部分无法释放; 虚析构:pa动态类型B,调用B的析构函数,先析构B特有部分,然后B的析构再调用A的析构,去析构A的部分,这样全部内存都被释放了; 与虚函数相同,虚析构也会被继承; 如果父类中的析构是虚析构,那么子类中的析构自动成为虚析构;无论子类是否显式定义了析构; 即使父类不需要析构函数,也要显式定义一个空的虚析构,以保证delete释放的正确执行; 虚析构可以被声明为纯虚析构,此时该类成为抽象类; 纯虚析构函数必须定义,而不能只声明不定义; 当释放子类对象时,必须调用父类的析构函数,如果调用到了这个未定义的纯虚析构,会报错;
|
||||||||||||||||||
|
|
预编译(必背)
1文件包含 |
把源文件中的#include扩展为文件正文; 也就是把包含的.h文件找到,并展开到#include所在之处; |
2条件编译 |
预处理器根据#if、#ifdef等编译命令及其后的条件,将源程序中某部分包含进来或者排除在外; 通常把排除在外的语句换成空行; |
3宏展开 |
预处理器将源程序文件中,宏的引用展开成相应的字符串; |
构造函数和析构函数
构造函数
|
构造函数是用来初始化对象的数据成员的; 构造函数没有返回值void也不行; 构造函数名称必须和类名称相同; 构造函数不能有const或者volatile修饰词; 不能用类的对象直接调用构造函数; class A{public:A(){}};A a;a.A();是错误的 A::A();A();a.A::A();都是正确的;
|
||||||||||||
默认构造函数 |
默认构造函数,有两种
|
||||||||||||
合成的默认构造函数 |
如果未自定义构造函数,编译器会自动合成一个无参的默认构造函数; 如果自定义一个非默认的构造函数,则编译器不会自动合成默认构造函数; 如果仅仅自定义一个非默认的构造函数,而没有定义默认构造函数,则该类没有默认构造函数; class A{};A a;该类有一个编译器合成的默认构造函数; class A{A(int i){}};该类没有默认构造函数A a;是错误的; 默认构造函数只能有一个;class A{public:A(){} A(int i=1){}};这里有两个默认构造函数,是错误的; |
||||||||||||
单形参构造函数 转换构造函数 |
单形参构造函数可以进行类型转换,将参数类型自动转换为该类的类型; class B{};class A{public:A(int i){}A(B b){}}; A a(9);A a=9;B b;A a(b);A a=b;a=(A)3,a=(A)b都是对的; 单形参构造函数可以支持隐式的或者显式的类型转换; |
||||||||||||
单形参构造函数 隐式类型转换
初始化、赋值 传参、返回值 多次转换 |
l 初始化:将类对象初始化为单形参类型时,执行隐式类型转换;A a=3;A a(3); l 赋值:将单形参类型赋值给类对象时,执行隐式类型转换;A a;a=3; l 传参:将单形参类型数据传递给需要该类对象的函数时,执行隐式类型转换; l 返回值:返回值为单形参类型,而定义的返回值类型为该类类型时;A f(){return 3;} l 多次转换:系统内置类型转换和构造函数一起转换; l f(A a){} f(2.2)先用内置转换或类型提升转换为int,然后再利用单形参构造函数进行类型转换为A类型; l 形参为const引用时,也会执行多次转换;void f(const A &a) f(3.14); l 如果为非const引用,则不会执行类型转换;void f(A &a) f(3.14);就是错误的; |
||||||||||||
explicit
只禁隐式 不禁显式 |
explicit关键字只能用来修饰构造函数的声明; 如果在类内声明构造函数时用了explicit,那么在类外定义构造函数则不能再用explicit; explicit用来禁止隐式类型转换,但是依然可以进行显式类型转换; class A{public:A(int i){}};A a=3;都是错误的;A a(3);A a=(A)3;是正确的; 为了避免不必要的类型转换和临时对象,所有的单形参构造函数都应使用explicit; |
||||||||||||
析构函数 |
析构函数用来释放对象所占用的资源; 析构函数和类同名,并前置一个~符号; 析构函数不能有任何形参,析构函数不能有任何返回值,void也不行; 无论是否自定义析构函数,编译器都是提供一个默认析构函数; 析构函数的调用顺序和构造函数相反; |
||||||||||||
自动调用析构函数 |
1) 对象生命周期结束时; 2) 对象被撤销时; 3) 对象离开作用域时; 4) delete对象的指针时; 5) 程序结束时; 当对象的指针离开作用域时,对象并不会析构; 调用自定义析构时,默认析构依然会被调用; |
||||||||||||
自定义析构函数 |
构造函数中使用new创建类成员时,析构函数需要使用delete释放; 构造函数中使用new[]创建类成员时,析构函数需要使用delete[]释放; 有指针成员时,需要自定义复制构造函数和复制运算符; |
||||||||||||
显式调用析构函数 |
使用布局new运算符时,需要显式调用析构函数; 使用new构造类成员,析构函数中有对应的delete时,不需要显式调用析构函数,否则会重复释放; |
||||||||||||
对象初始化 |
① 显式调用构造函数;A a=A();A a=A(1,2); ② 隐式调用构造函数;class A{public:A(int m,int n){}};A a(1,2); ③ 使用new动态创建对象;A *pa=new A(); ④ 使用单形参构造函数初始化对象;A a(3);A a=3; |
||||||||||||
默认构造函数初始化对象 |
内置类型成员不会自动初始化; 内置类型成员无法隐式初始化; class C{const int m; C() {}};//错误"C::C()" 未提供初始值设定项; class C{int m; C() {}};//警告:这里的m是随机值,要么显示初始化,要么构造函数中赋值; 如有默认构造,对象成员可以隐式初始化; 如无默认构造,对象成员无法隐式初始化; class A{public:int m};A a; 如果在局部创建a,则m是未初始化的随机值,如果在全局创建a,则m初始化为0; 在局部A a=A();m依然是随机值; class A{};A a();这种方式隐式调用默认构造是错误的,该语句会被解释成一个函数的声明; |
||||||||||||
初始化列表 |
l 位置:初始化成员列表在函数形参小括号之后,在函数体之前,也就是在小括号和大括号之间; class A{public:int a,b,c;A():c(3),b(5),a(1){}}; 按照声明顺序,先初始化a,然后初始化b,最后初始化c; l 顺序:初始化顺序取决于成员变量的声明顺序,而与初始化列表顺序无关; l 开始:初始化列表由:冒号开始;:c(3),b(5),a(1) l 形式:初始化项由成员变量名和小括号内的初始化值组成;c(3) b(5) l 分隔:多个初始化项,由,逗号隔开; class A{public:const int m;A(){m=1;}};是错误的,构造函数内的语句是赋值,不是初始化,而const类型无法赋值; class A{public:const int m;A(){}};是错误的,m没有初始化列表进行初始化,而内置类型不会自动初始化,则m永远无法初始化; 初始化列表无法初始化数组; 初始化列表无法初始化静态成员; |
||||||||||||
必须使用初始化表的情形 |
1. 没有默认构造函数的对象成员; 2. const成员; 3. 引用成员; 每一个构造函数都应该包含上述三种成员的初始化列表; |
||||||||||||
初始化列表和构造函数 |
构造函数的执行分2步,1隐式或显式初始化阶段,2计算阶段; 计算阶段:就是执行构造函数中的语句;(本质是赋值,而不是初始化) 显式初始化:是指使用初始化列表显式初始化; 隐式初始化:是指初始化表中未出现的成员,使用默认初始化规则进行初始化; 内置类型的隐式初始化: l 在局部,内置类型成员不会初始化; l 在全局,内置类型成员会被设置为0; l 内置对象成员如果没有默认构造,也不会初始化; class A{public:A(int i){}};class C {public: A a; C(){} };//错误:类 "A" 不存在默认构造函数; 构造函数是赋值,不是初始化; 赋值不是初始化,初始化列表才是真正的初始化; 全局对象的内置类型成员的值设置为0,这不是真正的初始化; 对象成员,总是在初始化阶段进行初始化; 对象成员的初始化早于构造函数内的赋值语句; 对象成员对象先于本对象而创建; 对象成员的构造函数先于本对象的构造函数而执行; |
||||||||||||
复制函数初始化 临时对象 |
形式:复制构造参数只有一个形参,该形参必须是本类对象的引用,一般都使用const修饰; class A{public A(const &A a){}} 本质:拷贝构造函数是一个特殊的构造函数; 作用:初始化对象;把一个已经创建的对象复制到新对象;A a1;A a2(a1); 复制初始化:调用复制构造函数; 直接初始化:调用与实参匹配的构造函数; 默认复制构造函数:如果没有自定义复制构造函数,编译器就会合成一个; 默认复制构造函数:将所有非静态成员逐个复制到新对象中; 对于对象成员,则调用该类的复制构造函数进行复制; 调用复制构造函数:A a1;A a2(a1);A a2=a1;A a2=A(a1); A a2(a1);A a2=a1;为隐式调用复制构造函数; 如果复制构造函数有explicit关键字,这两种写法都是错误的; A a2=A(a1);为显式调用复制构造函数; |
||||||||||||
赋值运算符 |
如果没有自定义的赋值运算符,则编译器会合成一个默认的赋值运算符; 工作原理:和复制构造函数类似,逐个成员进行赋值; 对于非数组成员,用常规方式逐个进行赋值; 对于数组成员,对每个数组成员进行赋值; 对于静态成员,不会赋值; 对于对象成员,调用该类的赋值运算符进行赋值; 赋值:是在已经存在的两个对象之间进行的,用一个已经存在的对象去改变另一个已存在的对象;调用赋值运算符函数; 复制:是在已经存在的对象和新创建对象之间,是用一个已经存在的对象初始化一个新创建的对象;调用拷贝构造函数;
只要产生了临时对象,就会调用一次析构函数以释放该临时对象; 函数按值传递对象时会调用拷贝构造函数,并且会生成一个临时对象; 函数返回一个类型的对象时,会调用拷贝构造函数,并且会生成一个临时对象; 对象传参,会生成临时对象; 对象返回,会生成临时对象;
|
||||||||||||
自定义复制构造 |
class A{A(A &a){} A(const A &a){}};带const和不带const的两个复制构造可以同时存在; 定义形参是该类类型,而不是引用的拷贝构造是错误的;class A{A(A a){}}; 自定义复制构造后,编译器不再合成默认的复制构造; 自定义复制构造,形参一般用const,而复制构造一般不用explicit; A a2=A(a1);A a2=a1;这两种写法是等价的;如果有explicit,这两种写法都错误; 若复制构造函数体为空,则数据成员没有初始化,是一个随机值; 数据成员有指针时,应当自定义复制构造函数; 自定义复制构造函数,应当复制指针所指向的对象,而不是复制指针的值(对象的地址); class A{public:int*p;int b;A(){a=1;p=&b;} A(const A& a)}; A::A(const A& a):p(&b){*p=*a.p;b=ma.b;} *p=*a.p;复制的是指针所指向的对象的值,而不是指针本身的值; |
||||||||||||
自定义赋值运算符 |
需要自定义复制构造时,也需要自定义赋值运算符; 需要自定义赋值运算符是,也需要自定义复制构造; 如果自定义赋值运算符,是默认赋值运算符的重定义,则调用自定义赋值运算符; 如果自定义赋值运算符,不是默认赋值运算符的重定义,则依然调用默认赋值运算符; class A{public:void operator =(int i){}};形参类型不是该类类型的引用,不符合赋值运算符的定义,因此不是对默认赋值运算符的重定义,依然调用默认赋值运算符; classA{public:void operator=(A&){}};形参是该类类型的引用,是对默认赋值运算符的重定义,会调用自定义的复制运算符; |
左值引用和右值引用
左值lvalue |
右值rvalue |
left value左边的值 |
right value右边的值 |
loactor value在内存中、有明确存储地址(可寻址)的数据 |
read value可以提供值的数据(不一定可以寻址,例如寄存器中的数据) |
// 左值引用& int num = 10; int &b = num; // 正确 int &c = 10; // 错误
|
// 右值引用&& int num = 10; //int && a = num; // 错误,右值引用不能初始化为左值 int && a = 10; // 正确 a = 100; cout << a << endl; // 输出为100,右值引用可以修改值 |
继承
继承的基本概念 |
继承是在现有类的基础上进行扩展; 继承是在现有类的基础上,增加了属于自己的类成员; 继承之后的类拥有之前类的成员; 继承之后的类也拥有自己的类成员; 父类、基类、超类:被继承的类; 子类、派生类:继承之后的类; 继承方式分为:公有继承、私有继承、保护继承; 继承方式的访问符为:public、private、protect; |
||||||||||||||||||
继承的语法 |
C++使用:冒号表示继承; 继承的访问控制符可以省略; 如果派生的是class,则默认私有继承;class A:B{}; 如果派生的是struct,则默认公有继承;struct A:B{}; 父类必须是已经定义好的类,只声明而未定义的类,不能作为父类;
|
||||||||||||||||||
子类成员 |
子类成员分类:从父类继承的成员、自己定义的成员; 子类可以像访问自己的成员一样访问父类成员; 但是,父类不能访问子类的成员,因为父类不知道子类有哪些成员; 私有成员无法继承,私有成员只属于父类,而不属于子类; |
||||||||||||||||||
继承成员的访问级别 |
public继承:父类成员访问级别不变 private继承:public→private,protect→private protect继承:public→protect 私有继承只能进行一级继承,继承后所有成员变为私有,无法再继承下去; 保护继承可以进行多级继承,代代相传; protect只有在继承时,才有实质性的用处; protect在没有继承的类中,根本无用,只用public和private就够了; |
||||||||||||||||||
恢复访问级别 |
使用using可以恢复继承成员的访问级别; 恢复后的访问级别只能到父类的级别,而无法超越; class A{protected:int a;void f();};class B:private A{protected:using A::a;using A::f;}; 使用父类类名和作用域解析运算符::; class A{protected:int a;void f();};class B:private A{protected:A::a;A::f;}; |
||||||||||||||||||
静态成员 友元 |
静态成员始终只有一个,无论继承多少次; 静态成员不能在子类中进行初始化; 友元不会在继承中传递; 父类是不是友元和子类是不是友元没有任何关系; |
||||||||||||||||||
不会被子类继承的 |
构造函数; 复制构造函数/拷贝构造函数; 析构函数; 赋值操作符函数; |
||||||||||||||||||
|
子类的构造函数不能初始化父类成员; class A{public:int m;}; class B:public:A{B():a(3){}};这是错误的; B中第一个:表示继承,第二个:表示访问权限,第三个:表示初始化列表; 子类构造函数只能初始化子类成员; 父类成员需要使用父类构造函数进行初始化; 构造函数内的语句是赋值,而不是初始化; 初始化列表才是真正的初始化; 父类的构造函数是由子类的构造函数进行调用的; 先用子类的构造函数调用父类的构造函数,初始化父类成员; 再用子类的构造函数初始化子类成员; 先构造父类→再构造子类;析构顺序与此相反;
|
||||||||||||||||||
子类构造调用父类构造 |
显式调用:通过构造函数的初始化列表的方式显式调用父类的构造函数; class A{public:A(int i)};class B:public A{public:int b;B(int j):A(j),b(j){}}; 隐式调用:如果子类的初始化列表中没有显式调用父类的构造函数,则会隐式调用父类的默认构造函数; 隐式调用的前提是父类要有默认构造函数,否则会报错; class A{public:A(int i)};class B{public:B(){}};错误,A没有默认构造函数,无法隐式调用; 如果父类没有默认构造函数,则子类必须在初始化列表显示调用父类的构造函数; class A{public:A(int i)};class B{public:B():A(3){}B(3){}}; B():A(3){}正确,B(3){}错误,因为A没有默认构造函数; 子类的构造函数类内声明,类外定义时,不能在声明时调用父类构造函数; 因为,声明构造函数时不能使用初始化列表; class A{};class B:public A{public:B();B(int i):A();};B::B(int i):A(){} 其中,在声明中使用初始化列表调用A的构造函数是错误的,而在定义时调用是正确的; 子类只能初始化它的直接父类,而不能声明父类的父类; class A{};class B:public A{};class C:public B{public:C():A(){}}; C在初始化列表中调用A的构造函数是错误的,因为A不是C的直接父类;
|
||||||||||||||||||
初始化顺序 |
在继承时,不管父类的构造函数出现在初始化列表的什么位置,或者没有显式调用父类构造函数,父类都会先于子类成员进行初始化; 如果是多重继承,则按照继承顺序依次初始化父类; 父类初始化完成后,会按照子类成员声明顺序(而不是在初始化列表中的位置),依次初始化; class A{};class B:public A{public:int m,n;B():n(1),A(),m(3){}}; 初始化顺序为父类构造A()→子类成员(先声明的先初始化m(3),后声明的后初始化n(1)); 当子类含有父类的类对象成员时,该成员当做普通成员进行初始化; 依然是先初始化父类,然后按照声明顺序初始化各个子类成员;
|
||||||||||||||||||
复制构造函数 赋值操作符函数 |
父类的复制构造函数,复制父类部分; 子类的复制构造函数,复制子类部分; 父类的赋值操作符函数,对父类对象进行赋值; 子类的赋值操作符函数,对子类对象进行赋值; 子类默认复制构造函数→隐式调用→父类默认复制构造函数;✔ 子类默认复制构造函数→隐式调用→父类自定义复制构造函数;✔ 子类自定义复制构造函数→显示调用→父类的默认复制构造函数;✔ 子类自定义复制构造函数→显示调用→父类的自定义复制构造函数;✔ 子类自定义复制构造函数→隐式调用→父类的默认构造函数;✖错误
子类默认赋值操作符函数→隐式调用→父类默认赋值操作符函数;✔ 子类默认赋值操作符函数→隐式调用→父类自定义赋值操作符函数;✔ 子类自定义赋值操作符函数→显示调用→父类的默认赋值操作符函数;✔ 子类自定义赋值操作符函数→显示调用→父类的自定义赋值操作符函数;✔ 子类自定义赋值操作符函数→隐式调用→父类部分无法赋值;✖错误
|
||||||||||||||||||
父类和子类的转换 |
父类指针可以指向子类对象;无需显示强制类型转换; 父类引用可以指向子类对象;无需显示强制类型转换; 子类对象拥有完整的父类副本;
子类指针可以自动转换为父类指针;此时指向子类中的父类部分; 子类引用可以自动转换为父类引用;此时指向子类中的父类部分; 转换后的指针无法再指向子类特有成员; 转换后的引用无法再指向子类特有成员; 不存在从父类到子类的转换(指针、引用),因为父类不知道子类有什么成员; 转换只存在于对象的指针和引用之间,而不存在于类对象之间; 这种转换相当于将子类中子类特有部分切除,而仅仅保留了父类部分; |
||||||||||||||||||
子类转父类 |
能否进行子类到父类的自动转换(指针、引用),关键看父类的公有成员能否访问; 公有继承:可以进行自动转换; 非公有继承:类外不可以进行自动转换; 子类内:始终可以进行自动转换,而与继承方式无关; 子类的友元函数内:始终可以进行自动转换,而与继承方式无关; 子类保护继承:则该子类的子类内,都可以进行自动转换; 私有继承:不可以在类外进行自动转换;✖
|
||||||||||||||||||
继承下的名称解析、名称隐藏、函数重载 |
子类作用域和父类作用域是两个作用域; 子类作用域嵌套在父类的作用域内; 如果在子类中找不到某个名称,就到父类中去找; 在子类中,子类的作用域优先级高于父类,也就是先找子类,再找父类; 子类成员会隐藏掉父类的同名成员(子类找到,就不去父类找了); 子类和父类的同名函数不会构成函数重载; 子类的函数会隐藏掉父类中的所有同名函数; class A{public:void f(int){} void f(int,int){}};class B:public A{public:void f(){}}; 子类中的f()不会和父类中的f(int)、f(int,int)构成重载; 子类中的f()会隐藏掉父类中的所有f函数; 函数重载的前提是,函数在相同的作用域内;(带类类型形参的函数除外) 父类和子类函数分属于两个作用域,因此无法构成重载; 子类无法继承父类的私有成员,但是父类的私有成员依然存在,在名称查找时依然可见; class A{private:int a;}; class B:public A{public:void f(){a=3;}}; 这里的a不会出现未定义的标识符错误,而是出现无法访问的错误; 未定义标识符 "a";成员 "A::a" (已声明 所在行数:7) 不可访问;
|
||||||||||||||||||
访问父类隐藏成员 |
1. 使用作用域解析运算符:: b.A::a=3; b.A::f(); 2. 使用using将子类名称引用子类中;using A::a; using A::f;using引入后的成员,将和子类成员在同一个作用域内,如果有同名函数,可以构成函数重载; 3. 类名+作用域解析运算符::A::a;A::f;
|
||||||||||||||||||
using注意事项 |
using引入的父类成员变量不能在子类中重新定义; using引入的父类成员变量可以在子类中重新定义; 可以覆盖从父类引入函数的某个版本; using后又被覆盖的父类函数,只能通过作用域解析运算符进行调用; |
C++各种数据类型的默认值
数值类型int/double/float/long |
0 |
char |
'\0' |
string |
"\0" |
bool |
0,也就是false |
数组名和指针的区别
用sizeof时的区别;
用&时的区别;
复杂声明分析规则
优先级规则 |
首先从未声明的标识符开始分析;注意区分声明标识符和形参; 小括号括起来的优先级最高,最内侧的小括号优先级最高; 后缀运算符比前缀运算符优先级更高; 几个连续的后缀运算符,运算顺序从左到右; 几个连续的前缀运算符,运算顺序从右到左; 类型限定词const和volatile的作用域:如果左边紧跟指针运算符,则作用于左边的指针运算符,否则作用于类型区分符(左右均可); 未声明的标识符:从左到右,第一个标识符就是未声明的标识符(先右后左,右左法则); |
右左法则 |
右左法则的实质:后缀运算符的优先级高于前缀运算符; 从未声明的标识符开始看,先看右边,再看左边; 向右看,要一直看到小括号或者没有运算符为止,然后再向左; 向右看,遇到小括号,就进入小括号里面,在小括号内部同样应用右左法则; |
第一次原则 |
复杂声明的标识符到底是什么?取决于第一次和她结合的运算符是什么; 如果与标识符第一次结合的运算符是*,那么该标识符就是一个指针; 如果与标识符第一次结合的运算符是(),那么该标识符就是一个函数; 如果与标识符第一次结合的运算符是[],那么该标识符就是一个数组; |
遇到原则 |
如果分析过程中遇到指针运算符*,那么剩余部分就是该指针指向的对象类型; 如果分析过程中遇到[],那么剩余部分就是数组中元素的类型; 如果分析过程中遇到(),那么剩余部分就是函数的返回值类型; |
无标识符的复杂类型分析
总原则 |
给复杂类型添加一个标识符,那么就变成有标识符的复杂声明分析问题了; |
标识符定位原则 |
如果既有前缀运算符也有后缀运算符,那么标识符肯定位于紧邻的前缀运算符和后缀运算符之间; 如果有多个前缀运算符和后缀运算符的配对,那么标识符肯定位于第一个配对; 若只有前缀运算符,那么标识符位于所有前缀运算符的右侧; 若只有后缀运算符,那么标识符位于所有后缀运算符的左侧; |
typedef
基本规则 |
typedef可以给指定类型取一个新名字,也叫自定义类型名; typedef不会产生新的类型,而是给指定类型取了一个名字(别名); typedef是存储类区分符,不能和其他存储类区分符同时出现; 用自定义类型声明变量时,如果有还有其他限定符,则标识符首先与其他限定符结合,最后与自定义类型名结合; typedef的自定义类型名和系统内置限定符相比,优先级更低; 例如,typedef int *T;const T a;则a先与const结合,再与T结合; |
简化复杂声明 |
从左到右,从外到内,层层剥茧; typedef应从左边开始对复杂类型进行简化,因为左边的优先级更低,而typedef优先级也更低; 从左边简化,可以保证简化部分的优先级低于未简化部分的优先级,保证了简化前后,运算顺序的一致性; 从低优先级开始简化,可以保证简化后的类型能够还原成原类型;
|
typedef简化步骤 |
首先找到最左边的类型区分符; 从类型区分符开始向右看,直到遇到小括号; 从最右边往左看,直到遇到小括号或者右边运算符分析完毕; 如果小括号是用来改变运算优先级的,则进入小括号进行左右分析; 如果有形参是复杂声明,则先简化形参,然后再简化其他部分; 一次可以简化一个或者多个运算符; 将简化后的类型重写声明(替代),然后再简化剩余部分; 整理简化后的声明,将相同的类型用同一个名称表示; |
左值和右值
左值 |
右值 |
原意:赋值运算符左边的东西 |
原意:赋值运算符右边的东西 |
有内存单元 |
可能没有内存单元 |
可以寻址 |
可能无法寻址 |
表示一段连续内存 |
表示内存中的数值 |
如果没有const就可以被赋值 |
数值,无法赋值 |
左值可以在左边 左值也可以在右边 |
右值只能在右边 右值不能在左边 |
变量是左值; 字符串是一个不可修改的左值; const变量是不可修改的左值; |
立即数是右值 函数返回值也是右值 |
左值和右值的转换
a=a+1;
左边的a是指a所指的内存单元;
右边的a是指a所指内存单元中的数值;
1先把a中的值从内存中抽取出来,a从左值变成右值;
把a中抽取的值和1相加后,再写入到a所指向的内存单元;
复杂的声明
int (*f())[]; |
f()是一个函数,()括号内没有参数,表示f是一个无参函数 *f()表示对f()的返回值进行解引用,然后得到外边的类型 括号外边内容是int[],表示返回值解引用后是一个int数组 因此函数返回值就是一个int数组的指针 综上,int (*f())[]表示一个无参的返回值是int数组指针的函数; |
int (*g())(); |
g()是一个无参函数; (*g())对函数返回值解引用 解引用的结果是一个函数int ■(); 因此,返回值就是int ■()这种函数的指针; 综上int (*g())()就是一个无参的,返回值是int ■()这种函数的指针的函数; |
int(* h[2])(); |
h[2]是一个数组; (* h[2])对数组元素解引用,就是得到数组的元素; 解引用后得到一个函数int ■(); 因此数组中存放的是函数的指针; 综上,int(* h[2])()就是一个存放int ■()这种函数的指针的数组; |
多义词
多义符号 |
声明变量时 |
对变量进行运算时 |
* |
表示指针类型 |
解引用 |
& |
表示引用类型 |
取地址 |
声明的语法
声明区分符/前缀运算符 顺序无所谓 |
声明符/后缀运算符 随便嵌套 |
|||
存储类区分符 |
类型限定词 |
类型区分符 |
声明符1 |
声明符2... |
extern static auto register typedef |
const只读的 volatile易变的 |
int float double bool void 枚举 结构体 联合类型 自定义类型 |
标识符 函数声明符() 数组声明符[] 指针声明符* 引用声明符& |
5种声明符可以相互嵌套 嵌套之后仍然是一个声明符,且是一个整体 |
最多一个 |
可以多个 |
有且只有一个 |
|
|
声明语法1
存储类区分符 |
类型限定词 |
类型区分符 |
声明符... |
声明语法2
声明区分符 |
声明符... |
声明语法3
前缀运算符 |
声明符 |
后缀运算符 |
指针和const 完全解析
int a = 1,b=2;
const |
声明指针 |
指针本身p |
指针的解引用*p |
修饰p |
int* const p1=&a; //指针本身p不可更改 |
*p1 = 33;//OK
|
//p1 = &b;// 错误 C3892 “p1” : 不能给常量赋值 |
修饰*p |
int const* p2 =& a; //指针的解引用*p不可更改 |
p2 = &b;//OK
|
//*p2 = 11; //错误 C3892 “p2” : 不能给常量赋值 |
const int* p3 = &a;//指针的解引用不可更改 |
p3 = &b;//OK
|
//*p3 = 22;// 错误 C3892 “p3” : 不能给常量赋值 |
|
修饰p和*p |
const int*const p4 = &a;//指针本身p不可更改,指针解引用*p也不可以更改 |
//p4 = &b;// 错误 C3892 “p4” : 不能给常量赋值 |
//*p4 = 22; // 错误 C3892 “p4” : 不能给常量赋值 |
int const* const p5 = &a;//指针本身p不可更改,指针解引用*p也不可以更改 |
//p5 = &b; // 错误 C3892 “p4” : 不能给常量赋值 |
//*p5 = 22;// 错误 C3892 “p4” : 不能给常量赋值 |
|
总结: l 如果const在*和p之间,表示const只修饰p,而不修饰*p,表示p不可更改,而*p可以更改; l 如果const在*左侧,表示将*p看做一个整体,const修饰*p而不是p,表示*p不可更改,而p可以更改; l 如果在*左侧和右侧分别放置一个const,两个const分别修饰p和*p,表示p和*p都不可更改; l p表示变量的指针,也就是变量的地址,p不可更改,也就是p的值或者p的指向不可更改; l *p表示指针所指的变量,*p不可更改,表示p指向的变量的值不可更改; l 将*p看做一个整体时,const可以放在类型限定词int的左侧或者右侧,二者等价; |