Modern Effective C++前期知识(一)


  • 💂 个人主页:风间琉璃
  • 🤟 版权: 本文由【风间琉璃】原创、在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,不能修改指向

在上面示例中,ab都是顶层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常用的场景。

  1. 常量变量

    使用const定义的变量在初始化后不能被修改。x的值为10,且在程序的其他地方不能改变x的值。

    const int x = 10;
    
  2. 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;
      
  3. 常量引用

    引用的值在初始化后不能改变ref是一个常量引用,它绑定到x,表示ref不能改变x的值。引用ref并不创建一个新的对象,它只是另一个名字引用x

    const int &ref = x;
    const int &ref = 20;  // 正确 常量引用可以绑定到临时对象,因为它不会修改这个对象,20是一个int临时对象
    int& ref = 10;        // 错误,非 const 引用不能绑定到临时对象,不能绑定到临时对象 
    
  4. 常量成员函数

    在类中,成员函数可以被声明为const,表示该函数不会修改对象的状态myFunction是一个常量成员函数,它不会修改类的任何成员变量。

    class MyClass {
    public:
        void myFunction() const; // 声明常量成员函数
    };
    

    常量成员函数的定义需要在成员函数的定义后添加const

    void MyClass::myFunction() const {
        // 函数体
    }
    
  5. 常量对象

    当创建一个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* ptrint const* ptr):本质是指针,指向的内容是常量,不能修改内容,可以修改指针指向的地址。
    • 指针常量int* const ptr):指针本身是常量,不能修改指向的地址,但可以修改指针指向的内容
    • 常量指针常量const int* const ptrint 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::move

class 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);
    
        // 将函数指针指向函数add
        funcPtr = &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++中,可以使用typedefusing关键字为函数指针起别名。

  • 使用 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

typedefusing 都可以用来为函数指针起别名,using 是C++11之后的推荐方式,因为它更直观和现代。

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值