指针的概念
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 = #
// 将 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,只是传递了一个简单的结构体对象。但是在实际业务中,可能会传递一个比较复杂的类对象,创建对象的开销会比较大,这就会导致创建了临时对象,如果业务中这种操作很多,就会创建大量的临时对象,而对象创建回收都是内存开销,会造成没必要的性能损耗。
这带来了以下几个好处:
-
避免对象的复制
: 通过传递引用,可以避免在函数调用时进行对象的复制操作,特别是当对象较大或复制开销较高时,可以提高程序的效率。 -
直接修改原始对象
:通过引用,函数可以直接修改原始对象的值,而不是在函数内部操作副本。这对于需要修改传入参数的函数非常有用,因为它们可以直接对原始对象进行操作,而无需返回修改后的值。 -
简洁性和可读性
:通过使用引用作为函数参数,函数调用的语法更简洁明了,同时也更容易理解函数的行为,因为它表明函数可能会对传入的参数进行修改。
引用作为函数返回值
下面是一个函数返回值是引用对象的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;
}
这样做的好处:
-
避免对象的复制
:通过返回引用,避免了在函数返回时进行对象的复制操作。特别是对于大型对象,这可以提高程序的效率。 -
直接返回原始对象
:通过引用返回,函数可以直接返回原始对象,而无需创建副本。 -
连续操作
:通过返回引用,可以实现连续操作。这样我们可以将多个函数调用串联起来,对同一个对象进行一系列的操作。 -
减少内存使用
:返回引用可以减少内存的使用,因为它避免了创建额外的副本。
避免返回的引用对象是局部变量
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;
是改变了引用指向的对象,但是不会改变初始化时绑定的内容。
因此,
引用变量初始化后就不能修改
的意思是,一旦引用变量与某个对象绑定后,无法再改变其绑定,但仍然可以通过引用变量来修改绑定对象的值。