C++基础回顾
C++的基础知识,用于面试前的突击。
编译内存相关
C++程序编译过程
C++程序有4个编译过程,分别是预处理、编译、汇编和链接。
预处理:负责处理以
#
开头的指令,像#include
就是将对应的文件原封不动的复制过来编译:负责将预处理好的
C++
源文件翻译为汇编代码,编译完成后得到.s
文件汇编:负责将汇编代码翻译为二进制的机器指令,把这些指令打包成可重定位目标程序
链接:负责将上面生成的可重定位目标程序和库文件等等打包成最终的一个可执行文件
其中链接分为两种
静态链接:链接的时候把静态链接库中的程序直接拷贝到最终的可执行文件,这个库在每个用到它的可执行文件中都拷贝了一份
静态链接库浪费空间,更新困难,优点是效率高
动态链接:程序在编译时只记录动态链接库的名字等一些信息,在运行的时候加载动态链接库,一个动态链接库只有一个对应的库文件
动态链接库节省空间、更新方便,但是每次执行的时候需要重定位,相比静态链接有一定的性能损失
C++内存管理
C++将内存进行了一个分区操作,将内存分为了 .text
.data
.bss
.rodata
堆区和栈区。
.text
存放机器代码.data
存放已初始化的静态变量和全局变量.bss
存放未初始化的静态和全局变量,以及所有被初始化为 0 的静态和全局变量.rodata
存放只读数据- 堆区 存放动态申请的内存空间,需要手动的申请和释放,或者程序运行结束操作系统释放
- 栈区 存放函数的局部变量、函数参数和函数的返回地址等信息,由程序自动释放
堆和栈的区别
- 申请方式上,堆需要通过
new/delete
关键字进行一个手动的释放,栈有程序自动的分配和回收 - 存放内容上,堆存放的是手动分配的数据,栈存放的是局部变量、函数的参数和函数的返回地址
- 分配方式上,堆上分配的数据是不连续的,栈上分配的数据是连续的
- 拓展上,堆向高地址拓展,栈向低地址拓展
- 效率上,由于堆是类似于一种链表在内存中呈现,所以分配和销毁都需要消耗一定的时间,因此堆效率比栈要低
全局变量、局部变量、静态全局变量、静态局部变量的区别
全局和局部是作用域的概念;静态和非静态是生命周期的概念
- 从作用域来看
- 全局变量作用于所有的源文件,当然,其他文件需要用
extern
关键字重新申明 - 静态全局变量作用于本文件,无法作用于其他文件
- 静态局部变量和局部变量作用于代码块中,静态局部变量只会初始化一次,而局部变量只在代码块中,离开了代码块就会被销毁
- 全局变量作用于所有的源文件,当然,其他文件需要用
- 从生命周期来看
- 静态变量和全局变量都会在程序的全生命周期,储存在
.bss/.data
,局部变量的生命周期在代码块中,存储在栈中
- 静态变量和全局变量都会在程序的全生命周期,储存在
全局变量定义在头文件中有什么问题
全局变量有全局作用域,当不同的文件定义了同一个名字的全局变量,然后不同的文件 #include
的话就会出现链接错误,就是一个变量定义了多次
解决方法有
- 全局常量作为外部变量,使用
extern
关键字说明这个变量在外部定义
对象创建限制在堆或栈
限制在堆中
1 |
|
限制在栈中
1 |
|
什么是内存对齐?为什么要进行内存对齐,有什么优点?
内存对齐的原则就是基本类型有多少字节,对应对象的首地址都必须是多少字节的倍数。例如 int
是4个字节的,那么结构体中 int
类型的地址就要是 4 的倍数,遇到不够的会补充填充字节。另外整个类/结构体的大小是这里面字节数最宽的基本类型对应字节数的整数倍。
1 |
|
优点
- CPU读取内存时,是一块一块读取的,使用对齐会提高内存访问效率
- 有些硬件不支持任意地址的数据访问
内存泄露
程序没有释放不用了的内存
- 程序编码错误,
new
的数据没有delete
- 程序异常,正常的分支无法对内存进行释放
- 无主内存,
new
得到的指针被赋到另外一个值了 - 类的析构函数不是虚函数,当用父类指针销毁时,子类的成员变量无法得到释放
- 隐式内存泄露,程序不断的申请内存,但是在程序结束的时候才进行释放,比如说申请大量的Socket资源但是没有释放
智能指针
在头文件 memory
中
智能指针作用
C++中堆的管理十分麻烦,申请和释放都是需要手动操作的,容易造成内存泄露、二次释放等问题,使用智能指针能够更好的管理堆内存
智能指针区别
有三种类型的智能指针 unique_ptr, share_ptr, week_ptr
unique_ptr
独占所指向的对象share_ptr
可以让多个指针指向同一个对象week_ptr
是一种伴随类,指向share_ptr
所管理的对象,这种指针不会改变所控制对象的生命周期
智能指针底层
智能指针主要是通过将对象以指针的方式放入一个类中,在这个类的构造函数中创建指针指向的对象,在构造类的析构函数中完成资源的释放。
智能指针使用中要注意的地方
不
delete
get()
返回的指针不用
get()
初始化或reset
另一个智能指针不要混合使用智能指针
使用
get()
返回的指针时,当最后一个智能指针失效时,对应的get()
返回的指针也就失效了智能指针还会产生 循环引用 的问题,两个指针相互指向对方的内存空间,导致内存无法释放。
1
2
3
4
5
6
7
8class B;
class A{ public: shared_ptr<B> b; }
class B{ public: shared_ptr<A> a; }
int main() {
A a; B b; // A::b, B::a useCount = 1
a.b = b; // A::b useCount = 2
b.a = a; // B::a useCount = 2
} // A::b, B::a useCount = 1,资源不会进行释放
声明和定义的区别
- 声明只是把变量的类型和名字告诉编译器,并没有分配内存。定义的话需要分配内存
- 可以多次声明(
extern
),但是只能有一次定义
面向对象
什么是面向对象?面向对象的三大特性
面向对象就是把对象抽象成类,类中包括成员变量和成员方法。
- 封装:类把具体的实现给隐藏起来,只对外提供接口。例如类的
public
部分就是对外的接口 - 继承:子类继承父类的成员变量和成员函数
- 多态:我理解的多态就是不同的对象对同一个行为有不同的表现方式。总共有两大类
- 静态的多态:包括函数的重载、泛型和模板编程++
- 动态的多态
重载、重写、隐藏的区别
重载:同一个可访问区内一个函数名,有着不同的参数
同一个可访问区,函数名相同,参数不同(返回参数不看)
重写:子类对父类的函数进行一个重新实现
子类与父类间,函数名和参数都相同,父类函数有
virtual
隐藏:派生类的函数会隐藏掉父类的同名函数,不管参数列表是否相同
子类与父类间,函数名相同
多态如何实现
多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。
- 在类中用 virtual 关键字声明的函数叫做虚函数
- 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应)
- 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。
关键字库函数
sizeof
和strlen
区别
sizeof
是一个运算符,strlen
是一个库函数- 适用类型上,
sizeof
既适用于类型,也适用于变量,strlen
只适用于char *
类型的变量 - 返回结果上,
sizeof
统计的是这个数据实际占用的字节大小,strlen
统计的是这个字符串的长度。char a[10] = "Hello"
,此时sizeof(a) == 10, strlen == 5
(strlen
不包括\0
) - 当传入的类型是
char *
,sizeof
返回指针的大小,strlen
返回字符串的长度
lambda表达式
lambda表达式是一个匿名函数
lambda表达式的基本形式是
1 |
|
捕获形式 | 说明 |
---|---|
[] | 不捕获任何外部变量 |
[变量名, …] | 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) |
[this] | 以值的形式捕获this指针 |
[=] | 以值的形式捕获所有外部变量 |
[&] | 以引用形式捕获所有外部变量 |
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
当定义 lambda 表达式的时候,编译器会生成一个匿名类,这个类的成员就是捕获的内容,它还重载了()
运算符。在运行的时候,lambda
表达式会生成一个匿名类的对象,如何调用它的 ()
表达式
explicit
用来禁止隐式转换
注意事项
- 只能用于单参数的构造函数
static
作用
- 作用于局部变量时,可以延长变量的生命周期,变量的生命周期将一直持续到程序运行借宿
- 作用于全局变量和函数时,可以限制变量和函数的作用域,将变量和函数限制在本文件中
- 作用于类的成员变量和成员函数时,可以不把这个类实例成对象就使用类对应的变量和函数
在类中使用的注意事项
- 静态成员变量的定义和初始化是在类外进行的,类内只进行声明,类外初始化的时候不能出现
static
关键字 - 静态成员变量被所有类的对象和子类的对象所共享
- 静态成员变量可以作为类的成员函数的参数,普通的成员变量不能
- 静态成员函数没有
this
指针,不能声明为const/volatile/virtual
函数,这些函数是通过改变this
指针来进行限定的 - 静态成员函数不能调用非静态成员变量和非静态成员函数,原因也是没有
this
指针
const
作用
const
修饰成员变量可以进行类型检查,节省内存空间,提高效率const
修饰函数参数可以保证参数不发生改变const
修饰成员函数可以保证这个类里面的成员变量不会发生改变
用法
const
成员变量const
成员变量只能在类内声明、定义和初始化,最好在类的初始化列表中进行初始化
const
成员函数const
不能修改成员变量的值,除非该变量有mutable
进行了修饰const
不能调用非静态的成员函数和成员变量
define
define
和 const
的区别
define
是预处理阶段进行的,const
是编译阶段进行的define
只是进行简单的替换,没有进行类型安全的检查define
进行的替换会在内存中有多个备份,而const
在程序运行中只有一份const
定义的变量在调试的过程中可以显示数值
define
和 typedef
的区别
typedef
在编译期间进行,有类型检查功能typedef
有作用域的限制- 指针的操作:
typedef
和#define
在处理指针时不完全一样
1 |
|
inline
工作原理
- 内联函数是函数在编译阶段,编译器将内联函数的函数体嵌入到调用内联函数的地方
- 普通函数是需要保护上下文,将返回地址等等数据压入栈中,把传入的参数存入寄存器中,然后更改PC指针到函数对应的地方,函数调用完毕后,还需要恢复现场,需要比较大的开销
使用方法
- 在类内声明类外定义加
inline
都可以
作用
- 减少函数开销
- 去除函数只能定义一次的限制
new
/ delete
new/delete
和 malloc/free
的区别
new/delete
会调用构造函数和析构函数,malloc/free
没有new/delete
无需指定分配空间的大小,编译器会自动计算,malloc/free
需要new
分配成果返回对应类型的指针,失败抛出bad_alloc
异常,malloc
分配成果返回void *
类型的指针,失败返回空指针
delete
实现原理
- 调用该对象所属类的析构函数
- 释放内存空间
union
union
总的来说就是对同一个数据用不同的方式译码
union
有若干个成员组成,但是只有一个有效的成员union
更改一个成员变量的值,其他成员变量的值都会更改union
的大小为所有成员中最大的,还应遵循内存对齐原则
使用方法
例如下面这个例子可以判断是小端法还是大端法
1 |
|
volatile
使用 volatile
关键字修饰变量表示这个变量很可能被编译器所不知道的因素更改,比如说多线程、中断和一些硬件。
编译器遇到 volatitle
修饰的变量时会停止对这个变量进行编译上的优化,同时使用的时候系统每次都从内存中读取这个数据,即使CPU刚刚已经读取过
使用场景
- 多线程中如果两个线程更改同一个变量时,如果编译器对它进行优化,就会出现一个正在使用内存中的变量,一个正在使用CPU寄存器中的变量,导致计算结果不符合预期。
volatile
的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。 - 当读取一些硬件设备寄存器的时候,他们的寄存器的值可能很快的改变,
volatile
可以保证编译器不会对这个访问进行优化
强制类型转换
static_cast
只能用于低风险的强制类型转换(只能用于可以进行隐式类型转换的情况,作用只是将隐式类型转换挑明),无法用于不同类型指针之间的转换、整形和指针之间的转换
void *
转换成其他类型的指针- 浮点、整数、字符的转换
- 父类转换为子类(这种情况是特例,隐式类型转换不能编译通过,但是
static_cast
可以) - 子类转换为父类
- 转换运算符
static_cast
主要功能是替换隐式类型转换,让代码可读性更好
const_cast
将常量转化为非常量,只针对指针、引用和 this
指针
1 |
|
dynamic_cast
负责子类和父类之间的转换。实现了一个检测功能,当发现不安全这种情况会判断转型失败,返回空指针,对应用类型的转换失败会抛出异常
dynamic_cast
采用了 RTTI
(运行时类型识别)这种技术来在运行时检测被转换的指针的类型,有额外的开销,因此建议只在父类指针转子类的情况下使用
RTTI
的实现依赖于虚函数,因此 dynamic_cast
只能在有虚函数的类转换过程中使用,否则会编译错误
1 |
|
reinterpret_cast
用于替换强制类型转换,是进行逐字节的重新翻译
类相关
虚函数与纯虚函数
虚函数是用 virtual
关键字修饰的成员函数,当子类继承有虚函数的类,并且重写了这个虚函数时,如果一个父类的指针指向子类的对象,那么这个指针在调用虚函数时,处理的函数时子类中对应的函数
纯虚函数时 virtual
关键字修饰,并且在函数声明的末尾用 =0
进行修饰的函数,有了纯虚函数的类叫做抽象类,这个类不能够实例化,子类继承抽象类后实现所有的纯虚函数才能够实例化
两者区别
- 虚函数可以直接使用,纯虚函数必须要在子类实现后才能够使用
- 纯虚函数可以只声明不实现,虚函数必须要实现
- 定义形式不同
虚函数实现机制
虚函数时通过虚函数表来实现的。如果一个类或者它的父类中有虚函数,它成员变量中就会有一个指向虚函数表的指针。当一个父类的指针指向一个子类的对象时,父类的指针就会通过这个虚表指针找到子类的虚函数表,从而调用子类定义的虚函数
虚函数表和类绑定,虚表指针和类的对象绑定
虚函数表相关
- 存放内容:虚函数的地址
- 建立时间:编译阶段
- 虚表指针位置:存放在对象内存空间最前面的位置
- 对象建立过程:程序在初始化子类对象的时候,会先调用父类的构造函数,然后当遇到了父类的虚函数,编译器将虚表指针指向父类的虚表,随后调用子类的构造函数,虚表指针被覆盖,指向子类的虚表
- 多继承的情况,继承了几个类就有几个类的虚函数表
类的构造
禁止类的构造函数使用
- 禁止类外使用:声明为
private/protect
- 禁止类外和类内使用:增加
= delete
修饰符
1 |
|
构造函数与析构函数定义为虚函数
构造函数一般不声明为虚函数
原因是构造函数的目的是为了创造对象,而虚函数需要使用对象中的虚表指针,才能实现对虚函数的调用。此时对象还没有创建,因此如果构造函数时虚函数就无法调用
析构函数一般声明为虚函数
原因主要时为了防止内存泄漏。当父类的指针指向子类的对象时,如果使用
delete
关键字对这个对象进行回收,只会调用父类的析构函数,子类中的成员变量就得不到释放,造成内存泄露
多重继承出现问题
数据冗杂
命名冲突
常常出现在菱形继承中
classDiagram Base1 <|-- Base2 Base1 <|-- Base3 Base2 <|-- Base4 Base3 <|-- Base4
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Base1 { public: int var1; };
class Base2 : public Base1 { public: int var2; };
class Base3 : public Base1 { public: int var3; };
class Derive : public Base2, public Base3 {
public:
void set_var1(int tmp) { var1 = tmp; }
// error: reference to 'var1' is ambiguous. 命名冲突
private:
int var4;
};
int main() {
Derive d;
return 0;
}
解决方法:
指定冲突的变量来自那个类
void set_var1(int tmp) { Base2::var1 = tmp; }
使用虚继承,保证类的成员变量只有一份
1
2class Base2 : virtual public Base1 { public: int var2; };
class Base3 : virtual public Base1 { public: int var3; };
拷贝构造函数必须为引用
拷贝构造函数在调用的时候,如果传入的是变量,将会先调用拷贝构造函数,这样就会造成无限制的递归,最终导致栈溢出
类对象的初始化顺序/析构顺序
- 调用基类的构造函数,多重继承按照派生类表的顺序进行
- 按照声明的顺序对类的成员变量进行初始化
- 调用自身构造函数
析构函数调用顺序与构造函数相反
使用成员初始化列表会快些的原因
对象的成员变量初始化在进入构造函数函数体之前完场,如果在构造函数内进行成员变量的初始化,这样成员变量就会产生两次拷贝,造成浪费。使用成员初始化列表就相当于是一步到位了
类内初始化和初始化列表一样,不过初始化列表优先级更高,有初始化列表的,类内初始化会自动屏蔽
实例化对象的几个阶段
- 分配空间,不同的对象分配的时机不同
- (虚表指针赋值)
- 初始化
- 赋值
C++构造函数
默认构造函数:没有任何参数的构造函数
拷贝构造函数:同一类型的对象进行拷贝
1
2
3
4
5
6Box(Box& other); // Avoid if possible--allows modification of other.
Box(const Box& other);
Box(volatile Box& other);
Box(volatile const Box& other);
// Additional parameters OK if they have default values
Box(Box& other, int i = 42, string label = "Box");移动构造函数:传入参数为右值引用的构造函数
委托构造函数:一个构造函数调用了另外一个个构造函数
1
2Box() {}
Box(int i) : Box() {} // 委托构造函数继承构造函数:子类用了基类的构造函数
1
2
3
4
5
6
7struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
//...
};
struct B : A { using A::A; }; //关于基类各构造函数的继承一句话搞定
空类的默认函数
对于一份空类来说,如果没有实例化,编译器不会为它生成任何函数,在使用的时候,会根据情况生成 6 类函数
1 |
|
友元注意事项
- 友元是单向的
- 友元关系不能被继承
- 友元关系不具有传递性,例如A是B的友元,C是A的友元,但是C不能访问B的私有变量
静态类型和动态类型,静态绑定和动态绑定
静态类型:对象在声明时的类型,在编译的时候确定,非虚函数的调用对象由静态类型决定
动态类型:指针和引用所指的类型,在运行时确定,虚函数的调用对象由动态类型决定
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期,非虚函数一般都是静态绑定
静态绑定的函数空指针也能调用
1
2
3
4
5struct A { void print() { cout << "A" << endl; } };
int main() {
A *a = nullptr;
a->print(); // 能够输出 A
}动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期,虚函数一般都是动态绑定
不要重新定义父类的非虚函数
不要定义基类虚函数的缺省参数, 否则会产生异常。因为缺省是静态绑定,虚函数是动态绑定
1
2
3
4
5
6
7
struct E { virtual void func(int i = 0) { cout << "E: " << i << endl; } };
struct F: public E { virtual void func(int i = 1) { cout << "F: " << i << endl; } };
int main() {
F* pf = new F(); E* pe = pf;
pf->func(); //F: 1 正常,就该如此;
pe->func(); //F: 0 调用了子类的函数,却使用了基类中参数的默认值!
}
编译时多态和运行时多态
- 编译时多态:指的是函数重载和模板(泛型编程)
- 运行时多态:指的是父类的指针指向子类的对象,从而通过这个指针访问子类的虚函数(虚函数)
浅拷贝与深拷贝
- 浅拷贝是在拷贝指针时,只拷贝了指针,并没有开辟新地址。当指针指向的对线被销毁后,拷贝后的指针就变成了一个无效的指针。访问其中的资源就会出现错误
- 深拷贝是指拷贝指针的时候申请一块新的地址用来存放拷贝的值。即使原先的对象被销毁掉,也不会影响深拷贝的值。
浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。
深拷贝需要自己实现
语言特性相关
值类别
左值和右值
左值就是可以取地址的值,左值可以标识一个对象,或者一个函数,又或者一个地址
右值就相当于是一个临时的对象,例如字面量和两个对象相加形成的没有名字的临时变量,右值很快就会被销毁
C++11 标准又引入了一个
xvalue
将亡值,是由右值引用的产生而引起的,它是通过“右值引用”产生的对象,有两类- 返回右值引用的函数的调用表达式
- 转换为右值引用的转换函数的调用表达式
graph TB
value --拥有身份--> glvalue[泛左值glvaule]
value --可以移动--> rvalue[右值rvalue]
glvalue --不能移动--> lvalue[左值lvalue]
glvalue --可以移动--> xvalue
rvalue --拥有身份--> xvalue[亡值xvalue]
rvalue --没有身份--> prvalue[纯右值prvalue]
左值引用和右值引用
A &
只能引用左值const A &
常量引用可以引用左值也可以引用右值A &&
只能引用右值
右值引用的目的
在C++11中,用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源,而当用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符来移动资源,从而避免拷贝,提高效率。
除此之外还有移动语义和完美转发两个功能
- 移动语义通过
move()
将左值变为右值,让左值也能通过移动构造函数来高效的进行 - 完美转发通过
forward()
实现参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
移动构造函数
- 有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
- 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;
- C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;
- 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;
move()
, forward()
指针相关
野指针和悬空指针
- 野指针:没有初始化的指针
- 悬空指针:指针指向的对象被销毁
NULL
和nullptr
区别
NULL
开源于 C 语言,是一条宏指令,内容为0,在 C++ 中定义为整数 0,无法做到与整数的区分nullptr
是关键字,它本身就是有类型的,没有二义性的问题
指针和引用的区别
- 引用是别名,在声明的时候就要初始化,指针不用
- 指针可以实现多极指针,引用只能有一级
- 指针在运行的过程中可以改变所指向的对象,引用不行
- 指针本身在内存中占有空间,而引用根据编译器的不同而不同
- 当编译器通过指针实现引用时,占内存
- 当编译器通过对象替换来实现引用时,不占内存
常量指针和指针常量
- 常量指针:指针指向一个常量,无法更改所指向对象的值
- 指针常量:指针是一个常量,无法更改所指向对象