掘金 阅读 ( ) • 2024-04-03 16:49

指针的概念

C++中每个变量都会分配一个内存地址,要访问变量的内存地址可以通过&运算符完成。而指针是一种变量,它指向了一个内存地址。指针允许我们间接地访问和操作内存中的数据。

指针的使用

最基础的指针

int main() {
    int a = 10;
    int *p = &a; //指针p指向a的内存地址

    cout << p << endl;
    cout<<"-------------"<<endl;
    *p =20; //修改指向的内存地址中的值
    cout << *p << endl; //输出:20
    cout << a << endl;  //输出:20

    cout<<"-------------"<<endl;
    int c =30;
    p = &c; //指针指向新的内存地址
    cout << p << endl;
    cout << *p << endl;
}

输出结果如下:

0x7ff7b9d6580c
-------------
20
20
-------------
0x7ff7b9d657fc
30

在上面代码中,*p = 20; 修改了指针所指向的内存地址中的值,而 p = &c; 改变了指针本身的指向,使其指向了一个新的内存地址。

指针变量和指向的变量类型必须要匹配

指针变量和指向的变量类型必须要匹配。比如下面的p1指向一个float类型的变量,这是不合法的。但是有一种特殊情况,可以将 int 类型的指针赋值给 char 类型的指针。

int main() {
    float a = 10.5f;
//    int *p1 = &a; //不合法,指针指向的变量的类型必须要匹配

    int num = 65;
    int *p2 = &num;
    // 将 int* 类型的指针转换为 char* 类型的指针
    char *p3 = reinterpret_cast<char*>(p2); 

    cout << *p2 << endl; // 输出 65
    cout << *p3 << endl; // 输出 A
    cout << (int )*p3 << endl; // 输出 65, ascii码A就是对应10进制数65
}

数组指针

数组指针本质上还是一个指针,只是说这个指针指向一个数组。数组指针的定义和使用如下

int main() {
    int arr[3] = {1, 2, 3};
    int (*ptr)[3]; //定义一个数组
    ptr = &arr; //将指针指向数组的地址

    cout << ptr << endl;
    for (int i = 0; i < 3; ++i) {
        cout << (*ptr)[i] << " ";
    }
    //输出:1 2 3
}

数组指针的另外一种实现方式

int main() {
    int *ptr2 = new int[3]{1, 2, 3};
    cout << ptr2 << endl; //输出:0x600003910030
    cout << (*ptr2) << endl; //输出:1
    cout << ptr2[0] << endl; //输出:1

    cout << *(ptr2 + 1) << endl; //输出:2
    cout << ptr2[1] << endl; //输出:2
    delete[] ptr2; //删除数组指针
}

上面代码中,通过new运算符为数组分配内存,指针ptr2指向的是数组第一个元素的地址。要访问数组可以通过如下方式:

//1.访问数组第一个元素
*ptr2
ptr2[0] 

//2.访问数组第2个元素
*(ptr2 + 1)
ptr2[1]

使用完数组后使用 delete[] 来释放动态分配的内存,以避免内存泄漏。

这里推荐使用第二种方式来定义数组指针。

指针数组

指针数组本质上是一个数组,其中的每个元素都是指针。

int main() {
    int a = 1, b = 2;
    int *ptrArr[2]; // 声明一个指针数组
    ptrArr[0] = &a;
    ptrArr[1] = &b;

    cout << ptrArr[0] << endl; //输出:0x7ff7b95c37ec
    cout << ptrArr[1] << endl; //输出:0x7ff7b95c37e8
    cout << *ptrArr[0] << endl; // 输出 1
    cout << *ptrArr[1] << endl; // 输出 2
}

小结

C++中的数组指针和指针数组是两个不同的概念。 数组指针是指向数组的指针,它指向数组的首个元素的地址,并可以通过递增指针来访问数组中的元素。指针数组本质是一个数组,其中的每个元素都是指针。

函数指针和指针函数

函数指针的概念

函数指针是指向函数的指针变量。它可以用于存储和调用函数的地址,使得我们可以在程序运行时动态地选择要调用的函数。

函数指针的声明方式如下:

返回类型 (*指针名字)(函数参数列表);

函数指针的简单demo

//定义一个函数
void printMessage(string message) {
    cout << message << endl;
}

int main() {
    // 声明一个函数指针,指向具有相同参数和返回类型的函数
    void (*ptr)(string);
    // 将函数的地址赋值给函数指针
    ptr = printMessage;
    // 通过函数指针调用函数
    ptr("Hello, world!");

    return 0;
}

函数指针的实际应用

函数指针可以用作回调函数的机制。在这种情况下,我们可以将一个函数的指针作为参数传递给另一个函数,以便在特定的事件发生时调用该函数。

void callbackFunc(int value) {
   cout << "callbackFunc value: " << value <<endl;
}

void func1(int value, void (*callback)(int)) {
    // 执行一些业务
    callback(value); // 调用回调函数
}

int main() {
    func1(10, callbackFunc);
    return 0;
}

此外,函数指针还可以用于实现多态行为,这个在后面的章节中讲。

指针函数

指针函数本质是一个函数,只是返回类型是指针类型。换句话说,指针函数返回一个指针,该指针可以指向不同的数据或对象。

// 指针函数
int *getNumberPtr(int num) {
    int *ptr = new int(num); // 动态分配内存并将 num 的值存储在其中
    return ptr; // 返回指向动态分配内存的指针
}

int main() {
    int number = 10;
    int *ptr = getNumberPtr(number); // 调用指针函数
    cout << *ptr << endl; // 输出指针所指向的值
    delete ptr; // 释放动态分配的内存
    return 0;
}

引用的概念与作用

C++的引用就是定义了一个变量,只不过这个引用类型的变量指向了另一个变量,引用与赋值的对象指向同一个内存地址,因此它们操作的是同一个对象。这样的好处是实现类似指针的功能,但是又能像使用普通变量那样去操作。引用提供了一种简洁的方式来操作变量,而无需使用指针或复制变量的值。 通过引用,我们可以直接访问和修改原始变量的值,而无需使用变量名。这一点在函数参数传参和函数返回值中特别有用。

引用的语法

Type& name = var

引用的特点

  • &在这里不是求地址运算符,而是起标识作用
  • 引用变量的类型和指向的变量的类型必须一致
  • 引用变量在声明时候就必须初始化
  • 引用变量初始化后就不能修改
  • 不能有NULL引用

最基本的引用使用

int main() {

    int a = 10;
    int &b = a; // 声明并定义一个引用b,它是a的别名

    cout << "a: " << a << endl;
    cout << "b: " << b << endl;

    b = 20; // 修改引用的值,也会修改原始变量的值
    cout << "---------修改引用b的值------------" << endl;

    cout << "a: " << a << endl;
    cout << "b: " << b << endl;

    cout << "---------a和b的内存地址------------" << endl;
    cout << "a: " << &a << endl;
    cout << "b: " << &b << endl;

    return 0;
}

输出结果:
a: 10
b: 10
---------修改引用b的值------------
a: 20
b: 20
---------a和b的内存地址------------
a: 0x7ff7b37bf808
b: 0x7ff7b37bf808

可以看到修改引用b的值,a的值也会修改。而且a和b的内存地址也是一样的,说明引用和赋值变量是操作的同一个对象。

到这里相信大家已经掌握了引用的概念和使用。但是聪明的你可能会有一个疑问,引用的作用好像就是起到一个别名的作用。就像demo中,我直接操作变量a不行吗?为什么要定义一个引用变量b呢?直接操作a不就可以了吗,定义b看上去是多此一举啊。到这里看上去确实没啥实际作用。别着急,后面的案例会解决大家的困惑。

引用作为函数参数

先来看如下代码,函数func1的参数是引用类型

//定义一个结构体
struct Student {
    string name;
    int age;
};

//函数参数是引用类型
void func1(Student &s) {
    cout << "func1 s age: " << s.age << endl;
    s.age = 31;
}

int main() {
    Student2 stu;
    stu.name = "lisi";
    stu.age = 30;

    func1(stu);
    cout << "stu age: " << stu.age << endl;

    return 0;
}

输出结果:
func1 s age: 30
stu age: 31

在这段代码中,调用func1函数相当于Student &s = stu,也就是说s和stu是操作的同一个对象。在func1中改变了age的值,也改变了stu对象的值,也验证了形参和传入的实参是同一个对象的结论。

那这样做的意义是什么?
上面这个demo,只是传递了一个简单的结构体对象。但是在实际业务中,可能会传递一个比较复杂的类对象,创建对象的开销会比较大,这就会导致创建了临时对象,如果业务中这种操作很多,就会创建大量的临时对象,而对象创建回收都是内存开销,会造成没必要的性能损耗。

这带来了以下几个好处:

  1. 避免对象的复制: 通过传递引用,可以避免在函数调用时进行对象的复制操作,特别是当对象较大或复制开销较高时,可以提高程序的效率。
  2. 直接修改原始对象:通过引用,函数可以直接修改原始对象的值,而不是在函数内部操作副本。这对于需要修改传入参数的函数非常有用,因为它们可以直接对原始对象进行操作,而无需返回修改后的值。
  3. 简洁性和可读性:通过使用引用作为函数参数,函数调用的语法更简洁明了,同时也更容易理解函数的行为,因为它表明函数可能会对传入的参数进行修改。

引用作为函数返回值

下面是一个函数返回值是引用对象的demo

//函数返回值是引用
Student &func2(Student &s) {
    s.age = 32;
    return s;
}

int main() {
    Student stu;
    stu.name = "lisi";
    stu.age = 30;

    Student &stu2 = func2(stu);
    cout << "stu age: " << stu.age << endl;
    cout << "stu2 age: " << stu2.age << endl;

    return 0;
}

这样做的好处:

  1. 避免对象的复制:通过返回引用,避免了在函数返回时进行对象的复制操作。特别是对于大型对象,这可以提高程序的效率。
  2. 直接返回原始对象:通过引用返回,函数可以直接返回原始对象,而无需创建副本。
  3. 连续操作:通过返回引用,可以实现连续操作。这样我们可以将多个函数调用串联起来,对同一个对象进行一系列的操作。
  4. 减少内存使用:返回引用可以减少内存的使用,因为它避免了创建额外的副本。

避免返回的引用对象是局部变量

string &func3() {
    string s = "abc";
    return s;
}

int main() {
    string &s2 = func3();
    cout << s2 << endl;

    return 0;
}

上面代码中func3返回的引用对象是局部变量。当函数执行完,局部变量对象也就被销毁,栈空间被释放,从而返回的地址已经不存在,导致后面执行出错。所以不要返回局部对象的引用。

引用的本质

引用的本质是一个常量指针。

int main() {
    int a = 10;
    int &b1 = a; //等价于 int const *b2 = &a;

    return 0;
}

在上面demo中,b1和b2是等价的。

C++ 编译器 在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。

常量指针的含义

常量指针可以修改指针所指向的对象(或者说内存地址),但是不能通过该指针修改所指向的对象的值。具体解释如下:

int main() {
    int a = 10;
    int const *p = &a;
    //非法操作,编译报错
    *p = 20; //常量指针。不能修改所指向的对象的值

    int c = 20;
    //合法操作
    p = &c; //常量指针可以改变指向的对象
    return 0;
}

引用变量初始化后就不能修改的解释

现在我们再来看下之前的一个问题,引用变量初始化后就不能修改。

int main() {
    int a = 10;
    int &b = a;

    int c = 20;
    b = c; //这里会不会报错呢?
    cout << b << endl;
    return 0;
}

我们之前说引用初始化之后就不能修改了,那是否意味着 b = c;这句代码会出现异常呢?实际上并不会,这里只是给变量b重新赋值罢了,b的输出是20。这有什么原因呢?

我们说过引用的本质是一个常量指针。那上面的代码可以转换成如下代码:

int main() {
    int a = 10;
    int &b = a; // 等价于 int const *p = &a;
    
    int c = 20;
    b = c; //等价于 p = &c;

    cout << b << endl;
    return 0;
}

可以看出 b = c; 等价于 p = &c; 将指针p 重新指向了变量 c是合法的。所以说b = c;是改变了引用指向的对象,但是不会改变初始化时绑定的内容。

因此,引用变量初始化后就不能修改的意思是,一旦引用变量与某个对象绑定后,无法再改变其绑定,但仍然可以通过引用变量来修改绑定对象的值。