文章目录
- 一、深入浅出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之后的推荐方式,因为它更直观和现代。