张德长

导航

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{public:A(){}};是默认构造函数

所有参数都有默认值的构造函数

class A{public:A(int i=1,int j=2){}};也是默认构造函数

 

合成的默认构造函数

如果未自定义构造函数,编译器会自动合成一个无参的默认构造函数;

如果自定义一个非默认的构造函数,则编译器不会自动合成默认构造函数;

如果仅仅自定义一个非默认的构造函数,而没有定义默认构造函数,则该类没有默认构造函数;

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计算阶段;

计算阶段:就是执行构造函数中的语句;(本质是赋值,而不是初始化)

显式初始化:是指使用初始化列表显式初始化;

隐式初始化:是指初始化表中未出现的成员,使用默认初始化规则进行初始化;

内置类型的隐式初始化:

在局部,内置类型成员不会初始化;

在全局,内置类型成员会被设置为0;

内置对象成员如果没有默认构造,也不会初始化;

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的左侧或者右侧,二者等价;

 

posted on 2022-12-14 17:03  张德长  阅读(54)  评论(0编辑  收藏  举报