文章目录
- 一、深入浅出const
- 1.1 顶层const
- 1.2 底层const
- 1.3 const
- 1.4 const注意事项
- 1.4.1 const与拷贝
- 1.4.2 const与拷贝构造函
- 1.5 总结
- 二、值类型与右值引用
- 2.1 值类型
- 2.1.1 左值(Lvalue)
- 2.1.2 纯右值(Prvalue)
- 2.1.3 将亡值(Xvalue)
- 2.1.4 **泛左值(Glvalue)**
- 2.2 左值引用与右值引用
- 2.2.1 **左值引用**(`Type&`)
- 2.2.2 **右值引用**(`Type&&`)
- 2.2.3 移动语义
- 三、数组与指针
- 3.1 指针数组与数组指针
- 3.2 数组名与指针
- 3.2.1 数组与指针的区别
- 3.2.3 **数组名退化为指针**
- 四、函数与指针
- 4.1 指针函数与函数指针
- 4.2 函数指针别名
- 💂 个人主页:风间琉璃
- 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有
帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
一、深入浅出const
在C++中,const关键字用于定义常量和控制对对象的修改。const具有多个使用场景,可以应用于变量、指针和函数参数,形成不同的含义,能够提高代码的安全性和可读性。根据其应用位置的不同,const可以分为顶层const(top-level const)和底层const(low-level const)。
1.1 顶层const
顶层const指的是对象本身的常量性,表示对象本身是不可变的(即对象本身不可以修改)。
- 对于非指针或非引用类型,该对象的值不能被改变
- 对于指针或引用类型,指针或引用的本身是常量(即指针的地址或引用的绑定是不可变的)
- 对于指针,顶层
const确保指针不能改变指向的地址 - 对于引用,顶层
const确保引用不能重新绑定到其他对象
- 对于指针,顶层
总之,顶层const表示变量本身是常量,也就是说,这个变量一旦被初始化之后就不能被修改。顶层const一般用于定义常量对象或变量,保护它们不被修改。
const int a = 10; // a 是顶层const,不可修改
int const b = 20; // b 也是顶层const,不可修改int x = 10;
int y = 20;
int* const ptr = &x; // ptr 是顶层const,不能指向其他地址
*ptr = 30; // 合法,ptr 指向的值可以被修改
ptr = &y; // 不合法,ptr 是顶层const,不能修改指向
在上面示例中,a和b都是顶层const,它们的值在初始化后不能再被修改。同时,顶层const适用于基本数据类型、指针和类对象等。
如果const变量是一个指针,顶层const表示指针本身是常量,不能指向其他地址,但指针所指向的内容可能是可变的,可以通过ptr修改它所指向的x的值,但不能改变ptr本身的值。
1.2 底层const
底层const涉及的是指针指向的数据不可变性,即指针指向的内容是否可以被修改。底层const描述的是对象的内容而不是指针本身。底层const表示变量所指向的对象是常量,即变量指向的内容不能被修改。底层const通常用于指针、引用或类成员函数。
const int* p = &a; // p 是底层const,p指向的值不能被修改 int const *p这两个是等价的int x = 10;
const int* p = &x; // p 是底层const
*p = 20; // 不合法,不能修改 p 指向的值
p = &y; // 合法,可以修改 p 的指向
在这个示例中,p是一个指向const int类型的指针,这意味着p指向的内容(即*p)是不可修改的。对于指针,底层const表示指针所指向的内容是常量,不能通过指针去修改这个内容,但指针本身可以指向其他地址。
注意,int const *ptr; 和 const int *ptr; 是等价的,都是底层const。
-
int const *ptr;表示ptr是一个指向const int的指针。
-
const int *ptr; 表示ptr是一个指向const int的指针。
这两种写法在C++中是完全等价的,都表示指针ptr指向的内容是const int类型,即ptr指向的int值是常量,不能通过ptr修改。
顶层const关注的是对象本身的常量性,而底层const关注的是对象内容的常量性。实际编程中,顶层const和底层const可能会一起出现,这时候变量本身和它指向的内容都不能修改。
const int x = 10; // x 是顶层const
const int* const p = &x; // p 是顶层和底层const
p本身是常量(第二个const是顶层const),p指向的内容也是常量(第一个const是底层const),因此p既不能指向其他地址,也不能修改指向的内容。
这里还有另外一种理解方法:const 右边靠近谁,谁就不可变,靠近就是指向的不可变,靠近变量命,就是该变量不可变。
const int *p; //const 修饰*p, p 是指针,*p 是指针指向的对象,不可变
int const *p; //const 修饰*p, p 是指针,*p 是指针指向的对象,不可变 1和2等价int *const p; //const 修饰 p,p 不可变,p 指向的对象可变
const int *const p; //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象
总结:
- 顶层const:使
对象本身的为常量,不能修改该变量的值或指针的地址,可以改变其指向对象内容而改变值。 - 底层const:使指针或引用所指向的
对象内容为常量,不能通过该指针或引用修改对象的值,可以改变其指向地址而改变值。
1.3 const
下面介绍一下const常用的场景。
-
常量变量
使用
const定义的变量在初始化后不能被修改。x的值为10,且在程序的其他地方不能改变x的值。const int x = 10; -
const修饰指针
在C++中,
常量指针和指针常量是两个不同的概念,它们的区别在于const修饰的是指针本身(顶层)还是指针指向的内容(底层),也和顶层const和底层const有关。-
顶层const:指针本身是常量,不能改变指向的地址。
ptr是一个常量指针,表示ptr指向的地址不能改变,但通过ptr可以修改x的值。即指针常量,一个指针本身是常量,也就是说,指针本身的值(即指向的地址)不能被修改,但指针指向的内容可以被修改。int *const ptr = &x; // *ptr = val; 修改值 类型* const 指针名; -
底层const(常量指针):指针指向的数据是常量,不能通过指针修改数据,但是可以通过修改指针指向地址改变值。也就是常说的
常量指针,其指的是一个指向常量的指针,通过这个指针不能修改它所指向的内容。const int *ptr = &x; // ptr = &y; 修改值 const 类型* 指针名; 类型 const* 指针名;在这个例子中,
ptr是一个指向常量int的指针,表示不能通过ptr修改x的值,但ptr可以指向其他int对象。 -
同时具有顶层和底层const的指针:
ptr既是一个常量指针(顶层const),也指向常量数据(底层const)。ptr不能修改指向的数据,也不能改变指向的地址。即常量指针常量,指针本身和指针指向的内容都不能被修改。const int *const ptr = &x;
-
-
常量引用
引用的值在初始化后不能改变。
ref是一个常量引用,它绑定到x,表示ref不能改变x的值。引用ref并不创建一个新的对象,它只是另一个名字引用x。const int &ref = x; const int &ref = 20; // 正确 常量引用可以绑定到临时对象,因为它不会修改这个对象,20是一个int临时对象 int& ref = 10; // 错误,非 const 引用不能绑定到临时对象,不能绑定到临时对象 -
常量成员函数
在类中,成员函数可以被声明为
const,表示该函数不会修改对象的状态。myFunction是一个常量成员函数,它不会修改类的任何成员变量。class MyClass { public:void myFunction() const; // 声明常量成员函数 };常量成员函数的定义需要在成员函数的定义后添加
const:void MyClass::myFunction() const {// 函数体 } -
常量对象
当创建一个
const对象时,不能通过该对象修改其状态。obj是一个const对象,表示obj的所有成员变量都不能被修改,不能通过该对象修改它的成员变量或调用非const成员函数。const MyClass obj;
1.4 const注意事项
1.4.1 const与拷贝
准则:当执行对象拷贝操作时,常量的顶层const不受什么影响,而底层const必须一致。
顶层const是指变量本身是常量,这意味着在对象拷贝过程中,顶层const不会影响拷贝操作本身,也就是说,无论源对象是否是const,都可以进行拷贝。
const int a = 10;
int b = a; // 顶层 const 被忽略,a 的值可以赋给 b
a是一个const整型变量,但在赋值给b时,a的顶层const并不影响赋值操作。b得到的是a的值,而不是const属性。
顶层const在拷贝操作中:可以拷贝const对象,或将const对象的值赋给非const对象,但反之不一定成立(即不能将非const对象赋值给const对象)。
底层const是指指针或引用所指向的对象是常量。在对象拷贝时,如果源对象的某个成员是底层const的,那么目标对象对应的成员也必须是底层const,否则拷贝操作会失败。
const int* p1 = nullptr; // p1 是指向 const int 的指针
int* p2 = p1; // 错误,不能将 const int* 赋给 int*
const int* p3 = p1; // 与p1 的底层const一致
1.4.2 const与拷贝构造函
当定义一个类时,如果类有成员变量是const或引用类型,必须显式定义拷贝构造函数。否则,编译器生成的默认拷贝构造函数无法正确处理这些成员。同时必须禁止使用赋值操作符,因为const成员在对象初始化之后,const成员不能被重新赋值,可以防止对象被错误地赋值。
class MyClass {
public:const int value;MyClass(int v) : value(v) {} // 初始化列表中初始化const成员// 必须提供拷贝构造函数MyClass(const MyClass& other) : value(other.value) {}// 禁止赋值操作符MyClass& operator=(const MyClass&) = delete;
};MyClass obj1(10);
MyClass obj2 = obj1; // 调用拷贝构造函数
obj2 = obj1; // 错误:赋值操作符被删除
value是一个const成员,必须在初始化时赋值。因此,如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,但它无法正确拷贝const成员。
注意:拷贝构造函数的参数类型在C++中通常被定义为const ClassNam&。原因如下:
-
避免不必要的拷贝:使用引用传递而不是值传递避免了对象的拷贝,从而提高了效率。引用仅传递对象的地址,而不会引发拷贝构造函数的递归调用。
-
保持原对象的不可变性:使用
const确保在拷贝构造函数内部无法修改源对象的状态,从而保护原对象的数据完整性。 -
允许拷贝
const对象:只有使用const ClassName&类型,拷贝构造函数才能接受const对象作为参数。否则,如果使用ClassName&,则无法拷贝const对象。
1.5 总结
- 常量变量:声明为
const的变量,初始化后不能被修改。 - const修饰指针:分为顶层const(指针本身不可修改)和底层const(指针指向的数据不可修改)。
- 常量指针(
const int* ptr或int const* ptr):本质是指针,指向的内容是常量,不能修改内容,可以修改指针指向的地址。 - 指针常量(
int* const ptr):指针本身是常量,不能修改指向的地址,但可以修改指针指向的内容。 - 常量指针常量(
const int* const ptr或int const* const ptr):指针和指针指向的内容都是常量,不能修改内容,也不能修改指针指向的地址。
- 常量指针(
- 常量引用:引用绑定的对象不能被修改。
- 常量成员函数:函数声明后加
const,表示不会修改对象的状态。 - 常量对象:对象声明为
const,其状态不能被修改。
二、值类型与右值引用
2.1 值类型
在C++中,左值(lvalue)和右值(rvalue)是两个基本的概念,用于描述表达式的值类型及其在内存中的位置。这两个概念对于理解C++的表达式求值、内存管理、资源管理等方面至关重要。
在C++中,表达式是由操作数和操作符组成的组合,可以产生一个值。表达式类型(表达式的值类别)指的是表达式在求值后所代表的对象的类型。
2.1.1 左值(Lvalue)
左值(Lvalue, “locator value”)是表示一个对象的地址的表达式。左值代表内存中的一个位置,它有一个持久的地址,能够在赋值语句的左边出现。其特点如下:
- 可寻址:左值可以取地址(通过
&操作符)。 - 可修改:左值通常用于修改数据的对象。
int x = 10; // x 是左值
x = 20; // 可以对左值进行赋值
int* p = &x; // 可以取得左值的地址
2.1.2 纯右值(Prvalue)
右值(Prvalue, Pure Rvalue)是表示一个临时的值的表达式,它没有持久的内存地址,通常是计算结果或常量,常常在赋值语句的右边出现。其特点如下:
-
不可寻址:右值通常
不能取得地址,无法对纯右值使用取地址操作符&,因为它们没有持久的内存位置。 -
临时性:右值通常是
临时的,生命周期较短。
int x = 10; // 10 是右值
x = 20 + 5; // 20 + 5 是右值(表达式的结果)
int* p = &20; // 错误,20 是右值,不能取地址
2.1.3 将亡值(Xvalue)
将亡值是表示即将被销毁的对象的表达式,通常与临时对象有关。将亡值主要与右值引用相关。其特点如下:
-
可取地址:可以使用取地址操作符获取其地址,但
对象的生命周期即将结束。 -
与移动语义相关:将亡值用于表示
可以被移动的资源。
std::string&& rref = std::move(std::string("Hello"));
//std::move返回的右值引用是一个将亡值,因为它标识的对象即将被销毁或移动。
2.1.4 泛左值(Glvalue)
泛左值是C++11引入的术语,代表左值和将亡值的统称。它包括可取地址的表达式(左值)以及即将被销毁的对象的表达式(将亡值)。其特点如下:
- 涵盖范围广:包括左值和将亡值。
- 与内存位置相关:泛左值表达式通常与具体的内存位置相关联。
理解与区分:
-
++i是左值,而i++是右值
-
解引用表达式*p是左值,取地址表达式&a是纯右值
-
a+b、a&&b、a==b都是纯右值
-
字符串字面值是左值,而非字符串的字面量是纯右值
-
函数返回值是右值,返回时会将值会存储在一个临时变量中,在赋值给其它变量
表达式类型总结:

-
左值(Lvalue):表示一个对象,具有内存地址,可以取地址和赋值。
-
纯右值(Prvalue):表示一个临时值,没有存储位置,生命周期短。
-
将亡值(Xvalue):表示即将被销毁的对象,通常与右值引用和移动语义相关。
-
泛左值(Glvalue):左值和将亡值的统称,表示一个存储位置或即将被销毁的对象。
-
右值(rvalue):包含将亡值,纯右值。
在C++的表达式中,左值和右值是基本的分类
-
左值:表达式指向一个存储位置,具有持久的内存地址。例如变量名、数组元素、函数返回的引用等。
-
右值:表达式表示一个临时值,没有持久的内存地址。例如字面量、临时对象、表达式的计算结果等。
2.2 左值引用与右值引用
2.2.1 左值引用(Type&)
左值引用是我们在C++中最常见的一种引用形式,用来引用一个左值对象。左值引用可以绑定到任何可以取地址的左值表达式上。其特点如下:
-
左值引用用于指向已有的内存地址,通常用于
传递大对象以避免不必要的拷贝。 -
左值引用不能绑定到右值(临时对象)上。
int x = 10;
int& ref = x; // ref 是左值引用,绑定到左值 x
ref = 20; // 修改 x
2.2.2 右值引用(Type&&)
右值引用是C++11引入的一种新的引用类型,用来引用右值(通常是临时对象)。右值引用允许我们修改这些临时对象,或将它们的资源“移动”到另一个对象中,从而避免昂贵的复制操作。用于实现移动语义和完美转发。其特点如下:
- 右值引用只能绑定到右值(临时对象)上,不能绑定到左值。
- 右值引用通常用于实现移动语义,它可以
有效地“窃取”临时对象的资源。
int&& rref = 10; // 10 是右值,rref 是右值引用
2.2.3 移动语义
移动语义(Move Semantics)是 C++11 引入的一个重要概念,旨在提高大型对象(特别是那些涉及资源管理的对象)的复制效率。移动语义允许资源从一个对象“移动”到另一个对象,而不是进行昂贵的复制操作。这种机制通过右值引用(right-value reference)和**移动构造函数(move constructor)**以及它们使用右值引用参数来表示对象资源的“移动”。来实现,它们使用右值引用参数来表示对象资源的“移动”。
std::string str1 = "Hello";
std::string str2 = std::move(str1); // 将 str1 的内容移动到 str2 中
当 std::move(str1) 被调用时,str1 中的资源(即它内部管理的字符数组)被“移动”到 str2 中。这意味着 str2 现在拥有了 str1 原有的资源,而 str1 的内部状态则变为未定义(但仍然是有效的字符串对象), 其状态是有效但不确定的,通常在标准库的实现中,str1 会成为一个空字符串或处于类似空的状态。

下面分析如何实现窃取资源的移动语义:
#include <iostream>
#include <utility> // For std::moveclass MyClass {
public:int* data; // 动态分配的资源int size; // 记录数组大小// 构造函数MyClass(int s) : size(s), data(new int[s]) {std::cout << "Constructor: allocated " << size << " ints." << std::endl;}// 析构函数~MyClass() {delete[] data; // 释放资源std::cout << "Destructor: released memory." << std::endl;}// 复制构造函数MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {std::copy(other.data, other.data + other.size, data); // 复制数据std::cout << "Copy Constructor: copied data." << std::endl;}// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {other.data = nullptr; // 确保原对象不再拥有资源other.size = 0;std::cout << "Move Constructor: moved data." << std::endl;}// 移动赋值运算符MyClass& operator=(MyClass&& other) noexcept {if (this != &other) { // 防止自我赋值delete[] data; // 释放当前对象的资源data = other.data; // 窃取资源size = other.size;other.data = nullptr; // 确保原对象不再拥有资源other.size = 0;std::cout << "Move Assignment: moved data." << std::endl;}return *this;}// 禁止复制赋值操作符MyClass& operator=(const MyClass& other) = delete;
};int main() {MyClass obj1(100); // 调用构造函数MyClass obj2 = std::move(obj1); // 调用移动构造函数MyClass obj3(200); // 调用构造函数obj3 = std::move(obj2); // 调用移动赋值运算符return 0;
}

-
移动构造函数
-
接受一个右值引用参数 (
MyClass&& other)。 -
将原对象的
资源指针直接赋值给新对象(如data = other.data;)。 -
将原对象的资源指针置为
nullptr,以防止在原对象析构时释放资源。
-
-
移动赋值运算符
-
首先,检查自我赋值(
if (this != &other))。 -
释放当前对象已有的资源。
-
从右值对象“窃取”资源(如
data = other.data;)。 -
将右值对象的资源指针置为
nullptr。
最后使用
std::move来触发移动语义,通过std::move将一个左值强制转换为右值,以便触发移动构造函数或移动赋值运算符。如果类中没有实现移动构造,std::move之后仍是拷贝。 -
移动语义的优势
- 避免不必要的资源复制:对于大型资源或复杂对象,这可以显著提高性能。
- 减少临时对象的开销:通过**“窃取”资源而不是复制**,可以减少临时对象的构造和析构开销。
注意左值引用和移动语义都可以减少不必要的复制操作,但它们的用途和场景有所不同:
- 左值引用(Lvalue Reference) 是用来绑定左值(持久存在的对象),通过引用传递对象而不是复制对象,可以避免对象在函数调用或赋值中的不必要复制。例如:
void process(const std::string& str) {// 通过左值引用传递,避免复制std::cout << str << std::endl;
}std::string s = "Hello";
process(s); // s 被通过左值引用传递,未发生复制
process 函数接受一个 const std::string& 参数,这使得 s 可以被传递给 process 而无需复制,减少了不必要的复制操作。
当我们需要避免复制但仍然保持对象的原始状态不变时,例如传递大对象给函数进行只读操作,使用左值引用是最佳选择。
-
移动语义 的主要作用是在
处理临时对象或即将销毁的对象时,避免复制并直接“移动”资源。它适用于那些将右值引用作为参数的函数,目的是将一个即将销毁的对象资源转移到另一个对象中,而不是简单地避免复制。当我们需要避免复制,并且可以破坏原对象(源对象后面不使用,因为移动后源对象的状态不能确定)以实现高效资源转移时,移动语义是最佳选择。例如,将一个临时对象的内容转移给另一个对象时,使用右值引用和移动语义更为合适。
总之,左值引用避免了复制但不改变对象的所有权。移动语义避免了复制并转移资源的所有权,适用于对象即将销毁或转移所有权的场景。两者都是减少不必要复制的手段,但移动语义在处理临时对象或需要转移资源时更为有效。
三、数组与指针
3.1 指针数组与数组指针
-
指针数组是一个数组,其中每个元素都是一个指针。声明方式如下:int* ptrArray[5]; // 声明一个包含5个指针的数组,每个元素是一个指向int的指针从运算符优先级判断:[] > () > (解引用操作符)。
由于 []的优先级高于,
ptrArray[5]先被解析为一个数组,其中ptrArray是数组名,[5]表示这个数组有 5 个元素。由于数组的每个元素的类型为int*,表示每个元素是一个指向int的指针。因此,ptrArray是一个数组,数组的每个元素是一个int*类型的指针。
示例:
int a = 1, b = 2, c = 3;
int* ptrArray[3] = {&a, &b, &c}; // 初始化指针数组,指向不同的整型变量// 访问指针数组中的元素
for (int i = 0; i < 3; ++i) {std::cout << *ptrArray[i] << std::endl; // 输出1, 2, 3
}
ptrArray 是一个包含 5 个元素的数组,每个元素是一个 int* 类型的指针。每个指针元素可以指向不同的变量或数组的元素。可以用来管理多个独立对象的地址。因此,当需要存储多个不同对象的地址时,可以使用指针数组。
-
数组指针是一个指针,它指向一个数组的起始位置。声明方式如下:int (*arrPtr)[5]; // 声明一个指向包含5个int类型元素的数组的指针从运算符优先级判断:[] > () > (解引用操作符)。
由于
()的优先级高于*,arrPtr先与()结合,表示 arrPtr是一个指针。*arrPtr的类型是int[5],意味着arrPtr是一个指向包含 5 个int` 元素的数组的指针。
示例:
int arr[5] = {1, 2, 3, 4, 5};
int (*arrPtr)[5] = &arr; // arrPtr 是一个指向数组的指针// 通过数组指针访问数组元素
for (int i = 0; i < 5; ++i) {std::cout << (*arrPtr)[i] << std::endl; // 输出1, 2, 3, 4, 5
}
arrPtr 是一个指针,指向一个包含 5 个 int 元素的数组。数组指针可以用来访问整个数组,保留数组的完整性(包括大小信息)。通过数组指针可以直接操作整个数组。通常用于多维数组的处理,如下所示:
int matrix[3][4];
int (*matrixPtr)[4] = matrix; // 指向二维数组中一行的指针
3.2 数组名与指针
3.2.1 数组与指针的区别
在学C语言的时候,大部分老师都会说:”数组名就是指针”。但这种说法是错误的!
数组名本质上是一个常量(固定)指针,它代表数组的起始地址,但它不是一个真正的指针变量,数组名的地址是固定的,无法改变。数组名的类型是一个完整的数组类型,比如 int[5]。指针是一个变量,存储内存地址,可以通过赋值操作指向不同的内存位置。指针的类型是 T*,其中 T 是指针指向的对象的类型,比如 int*。这两者有一定的关联的,数组名在特定上下文中可以“退化”成指针,表示指向数组首元素的地址,但数组名本身并不是一个指针。
#include <iostream>int main() {int arr[5] = {1, 2, 3, 4, 5};// 打印数组名和数组首元素的地址std::cout << "arr: " << arr << std::endl;std::cout << "&arr[0]: " << &arr[0] << std::endl;// 打印数组的地址std::cout << "&arr: " << &arr << std::endl;// 尝试改变数组名指向的位置int* ptr = arr; // 指针可以指向数组的首元素ptr++; // 改变指针的指向std::cout << "ptr++: " << ptr << std::endl;// 尝试对数组名进行相同操作// arr++; // 错误:数组名是常量指针,不能修改其指向// 使用 sizeof 运算符比较数组名和指针的大小std::cout << "sizeof(arr): " << sizeof(arr) << " bytes" << std::endl;std::cout << "sizeof(ptr): " << sizeof(ptr) << " bytes" << std::endl;return 0;
}

数组名 arr 会退化为指向首元素的指针,因此打印 arr 和 &arr[0] 时会显示相同的地址。&arr 返回的是整个数组的地址,其类型是 int (*)[5],虽然地址值相同,但类型不同。然后指针 ptr 可以通过 ptr++ 来指向下一个元素。然而,尝试对数组名进行类似操作会导致编译错误,因为数组名是常量指针,不允许修改。最后使用sizeof,一个返回整个数组的大小,一个返回指针的大小,通常为 8 字节(在64位系统上),这表明数组名 arr 和指针 ptr 是不同的。
在说一说数组名a和数组地址&a的区别:
**数组名 a **:
- 数组名
a本质上是数组的首地址的常量表达式。 - 在大多数情况下,数组名
a会退化为指向数组第一个元素的指针,类型为T*,其中T是数组元素的类型。例如,int a[5]中,a退化为类型为int*的指针,指向a[0]。 - 数组名
a是不可修改的常量,无法通过赋值操作改变它所指向的位置。 a + i表示数组中第i个元素的地址。
**数组地址 &a **:
-
&a表示整个数组的地址,而不仅仅是第一个元素的地址。 -
&a的类型是T (*)[N],其中T是数组元素的类型,N是数组的大小。对于int a[5];,&a的类型是int (*)[5],表示指向一个包含 5 个int元素的数组的指针 -
&a是数组整体的地址,而不是单个元素的地址。 -
&a + 1表示跳过整个数组的内存位置,而不是仅仅跳过一个元素的内存位置。比如在一个int[5]数组中,&a + 1会跳过 5 个int的内存空间。
| 数组名a | 数组地址&a | |
|---|---|---|
| 类型不同 | a 的类型是 T*,即指向数组首元素的指针 | &a 的类型是 T (*)[N],即指向整个数组的指针,数组指针 |
| 含义不同 | a 退化为指向数组首元素的指针,表示数组首元素的地址 | &a 表示整个数组的地址,指向的是整个数组的起始位置 |
| 内存布局与访问 | 由于 a 表示首元素的地址,a + 1 指向数组的第二个元素(即 a[1] 的地址) | &a + 1 指向下一个数组块的起始位置。比如 int a[5] 中,&a + 1 指向了下一个 int[5] 数组块的位置。 |
#include <iostream>int main() {int a[5] = {1, 2, 3, 4, 5};std::cout << "a: " << a << std::endl; // 输出数组首元素地址std::cout << "&a: " << &a << std::endl; // 输出整个数组的地址std::cout << "a + 1: " << a + 1 << std::endl; // 输出第二个元素的地址std::cout << "&a + 1: " << &a + 1 << std::endl; // 输出整个数组之后的位置return 0;
}

输出结果如上图所示,a 和 &a 都指向相同的内存地址,但它们的类型不同。a + 1 移动到数组中的下一个元素地址,而 &a + 1 跳过整个数组,指向下一个数组块的起始地址。
小结:
-
a表示数组首元素的地址:是类型为T*的指针,表示数组第一个元素的地址,可以退化为指针并用于指针运算。 -
&a表示整个数组的地址:是类型为T (*)[N]的指针,指向整个数组,通常用于指针变量需要指向整个数组的场景。
3.2.3 数组名退化为指针
数组名在大多数表达式中表示一个指向数组首元素的指针,而不是整个数组。这意味着,虽然数组本身是一个对象,但在使用数组名时,编译器会将其解释为指向该数组首元素的地址的指针。
虽然在某些上下文中,数组名会退化为指向其首元素的指针,这种行为使得数组名可以被像指针一样使用,但仍需注意它们之间的差异。
数组退化为指针的常见场景
- 作为函数参数传递
当数组作为函数参数传递时,数组名会自动退化为指向数组首元素的指针。因为C++中无法直接传递整个数组,所以只能传递指向数组的指针。
void printArray(int* arr, int size) {for (int i = 0; i < size; ++i) {std::cout << arr[i] << " ";}
}int main() {int myArray[5] = {1, 2, 3, 4, 5};printArray(myArray, 5); // 数组名 myArray 退化为指针,指向数组首元素return 0;
}
myArray 作为参数传递给 printArray 函数时,退化为指向 int 的指针,即 int*。
- 在表达式中使用
当数组名出现在某些表达式中时,它也会退化为指针。例如,给指针赋值时,数组名自动退化为指向数组首元素的指针。ptr 将指向 myArray 的首元素,即 ptr = &myArray[0];。退化后的指针可以进行指针运算,比如加减操作。通过这些操作,可以遍历数组的元素。
int myArray[5] = {1, 2, 3, 4, 5};
int* ptr = myArray; // 数组名 myArray 退化为指针
std::cout << *(ptr + 2) << std::endl; // 输出3,相当于访问myArray[2]
数组不会退化为指针的场景
虽然数组名在很多情况下会退化为指针,但有一些特定场景除外:
- 使用
sizeof运算符
当使用 sizeof 运算符时,数组名不会退化为指针,而是返回数组的实际字节大小。
int myArray[5];
std::cout << sizeof(myArray) << std::endl; // 输出数组总大小,通常为20(假设int为4字节)
- 使用
&运算符
当对数组名使用取地址运算符 & 时,得到的是整个数组的地址,而不是首元素的地址。arrPtr 是一个指向包含5个元素的数组的指针,而不是一个指向 int 的指针。
int myArray[5];
int (*arrPtr)[5] = &myArray; // 获取数组的地址
- 使用
decltype运算符
decltype 运算符会获取数组的原始类型,而不会退化为指针。decltype(myArray) 返回的是 int[5] 类型,而不是 int*。
int myArray[5];
decltype(myArray) anotherArray; // anotherArray 的类型是 int[5]
既然数组名有时会退化为指针,那里有什么优劣呢?
-
数组大小信息丢失:当数组退化为指针时,数组的大小信息将丢失。例如,函数接收的只是指针,不知道数组的具体大小,因此通常需要额外传递数组的大小信息。
-
性能优势:数组退化为指针后,传递给函数时,只需要传递指针(通常是4或8字节),而不是整个数组。这样可以提高效率。
四、函数与指针
4.1 指针函数与函数指针
-
指针函数是返回类型为指针的函数,即函数的返回值是一个指针。声明指针函数时,需要将函数的返回类型定义为指针类型。形式如下:
返回类型* 函数名(参数类型列表);
指针函数常用于返回动态分配的内存地址,或者在函数内部处理指针并返回某个内存地址。
-
函数指针是指向函数的指针,即存储函数地址的变量。通过函数指针,可以调用所指向的函数。声明一个函数指针时,需要指定函数的返回类型和参数类型。函数指针的声明形式如下:
返回类型 (*指针名)(参数类型列表);#include <iostream>// 定义一个普通函数 int add(int a, int b) {return a + b; }int main() {// 声明一个指向函数的指针,指向一个返回int并接收两个int参数的函数int (*funcPtr)(int, int);// 将函数指针指向函数addfuncPtr = &add;// 使用函数指针调用函数int result = funcPtr(3, 4); // 等价于 add(3, 4)std::cout << "Result: " << result << std::endl;return 0; }函数指针常用于实现回调机制。例如,排序函数可以通过函数指针指定自定义的比较方式。
函数指针与指针函数的区别
函数指针:是一个变量,存储的是函数的地址,可以用来调用指向的函数。int (*funcPtr)(int, int);
指针函数:是一种函数,其返回类型是指针,用于返回内存地址或指针。int* getMax(int* a, int* b);
4.2 函数指针别名
在C++中,可以使用typedef或using关键字为函数指针起别名。
-
使用
typedef为函数指针起别名typedef 返回类型 (*别名)(参数类型列表);#include <iostream>// 使用 typedef 为函数指针类型起别名 typedef int (*FuncPtr)(int, int);// 定义一个普通函数 int add(int a, int b) {return a + b; }int main() {// 使用别名来声明函数指针FuncPtr ptr = &add; // int (*funcPtr)(int, int);// 调用通过函数指针调用函数int result = ptr(3, 4);std::cout << "Result: " << result << std::endl;return 0; }typedef int (*FuncPtr)(int, int);将函数指针类型int (*)(int, int)起了一个别名FuncPtr。之后就可以直接使用FuncPtr来声明函数指针ptr,并将其指向add函数。通过ptr来调用add函数,得到了结果。 -
使用
using为函数指针起别名
using 关键字是C++11引入的一种更现代的语法,它可以用来定义类型别名,语法上比 typedef 更清晰和直观。
using 别名 = 返回类型 (*)(参数类型列表);
#include <iostream>// 使用 using 为函数指针类型起别名
using FuncPtr = int (*)(int, int);// 定义一个普通函数
int add(int a, int b) {return a + b;
}int main() {// 使用别名来声明函数指针FuncPtr ptr = &add;// 通过函数指针调用函数int result = ptr(3, 4);std::cout << "Result: " << result << std::endl;return 0;
}
using FuncPtr = int (*)(int, int); 定义了函数指针类型 int (*)(int, int) 的别名 FuncPtr。
typedef 和 using 都可以用来为函数指针起别名,using 是C++11之后的推荐方式,因为它更直观和现代。
