C++中新引入的概念:引用
他们各有各自的优劣之分
那么我们在编程过程中,如何去抉择引用和指针?
接下来,我们将从指针和引用的区别和联系,以及他们的各自在编程实现中的优劣势。
1.指针和引用的定义
引用:引用是已存变量的一个别名,对引用的操作就是对原变量的操作
指针:在计算机中所有数据存储在内存单元中,而每个内存单元都有一个对应的地址,只要通过这个地址就能找到对应单元中存储的数据。
2.指针和引用的初始化
(1)指针的初始化
C语言中提供了取地址运算符&来表示变量的地址。其一般形式为: &变量名;
指针变量初始化的方法有两种:定义的同时进行初始化和先定义后初始化
1.定义的同时进行初始化:
int a = 1;
int *pa = &a;
2.先定义再初始化:
在定义指针变量的时候,表示这个变量是一个指针,也就是说只有与类型名称(比如int、float 等)搭配使用的时候才表示是指针变量,其他时候有不同的含义
int a = 5;
int *pa, *pb, &pc;
pa = &a;
*pb = &a;//error,此时pb已经表示指针了,再加*表示将指针指向的存储区域的内容取出来
注意:
- 多个指针变量可以指向同一个地址
- 指针的指向是可以改变的
- 指针的类型要和变量的类型一致
int a = 1;
int b = 2;
int *pa = &a;
int *pb = &a;//多个指针变量可以指向同一个地址
pb = &b;//指针的指向是可以改变的
//double *p_a = &a;//error
(2)引用的初始化
格式:类型 &引用名 = 变量名;
引用与基本类型
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int& r_a = a;//r_a引用a,r_a就是a的别名
cout << "&a = " << &a << ",a = " << a << endl;
cout << "&r_a = " << &r_a << ",b = " << r_a << endl;
r_a = 2;
cout << "a = " << a << endl;//2
cout << "r_a = " << r_a << endl;
a = 3;
cout << "a = " << a << endl;//3
cout << "r_a = " << r_a << endl;
return 0;
}
引用和类类型
#include<iostream>
using namespace std;
class A
{
public:
A(int i) : m_i(i){ }
void print()
{
cout << "m_i = " << m_i << endl;
}
private:
int m_i;
};
int main()
{
A a(10);
A& r_a = a;
a.print();
r_a.print();
return 0;
}
3.引用在实际编程中的使用
(1)常引用
使用引用时,由于可以通过引用去修改变量的值,所以为了防止出现这种情况,可以将引用定义为常引 用。
定义引用时加const修饰,即为常引用,不能通过常引用修改引用的目标
const 类型 &引用名 = 变量名;
类型 const &引用名 = 变量名;
//const在&前即可
示例 对象的常引用和常对象一样,只能调用常函数
#include<iostream>
using namespace std;
class A
{
public:
A(int i) : m_i(i){ }
void print() const
{
cout << "m_i = " << m_i << endl;
}
void setValue(int i)
{
m_i = i;
}
private:
int m_i;
};
int main()
{
A a(10);
const A& r_a = a;
a.setValue(20);
a.print();
//r_a.setValue(20);//常引用和常对象一样,只能调用常函数
r_a.print();
int a1 = 1;
const int& r_a1 = a1;
cout << r_a1 << endl;
a1 = 2;
cout << r_a1 << endl;
//r_a = 3; //error 不能通过常引用修改引用的目标
return 0;
}
普通引用只能引用左值,常引用也叫万能引用,既能引用左值,也能引用右值
左值和右值
左值:可以放在赋值运算符(=)左侧,一般普通的变量都是左值,表示了一个占据内存中某个可识别 的位置 (也就是一个地址)的对象。
普通的变量
赋值表达式结果
前++,--表达式结果
右值:只能放在赋值运算符(=)右侧,一般常量都是右值,在内存中不占据位置
常量
大多数表达式的结果
函数返回链式变量(将亡右值,即函数返回值)
#include<iostream>
using namespace std;
int func(void)
{
int num = 30;
cout << "&num = " << &num << endl;
return num;//临时变量保存num
}
int main()
{
//res = 临时变量
const int& res = func();
cout << "&res = " << &res << endl;
cout << res << endl;//30
int a = 3, b = 5;
//a + b = 10;Error
(a += b) = 10;
cout << a << endl;//10
++a = 20;
cout << a << endl;//20
++++++++a;//ok
cout << a << endl;//24
//a++ = 30;//Error
return 0;
}
(2)引用型函数参数
将引用用于函数的参数,这时形参就是实参的别名,可以通过形参直接修改实参的值,同时避免参数值 传递过程,减小函数调用开销。
#include<iostream>
using namespace std;
void swap1(int* a, int* b)
{
int tmp = 1;
int* p_tmp = &tmp;
*p_tmp = *a;
*a = *b;
*b = *p_tmp;
}
void swap2(int& a, int& b)
{
int tmp = 1;
tmp = a;
a = b;
b = tmp;
}
void swap3(int a, int b)
{
int tmp = 1;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 3, b = 5;
cout << "a =" << a << ", b = " << b << endl;
//swap1(&a,&b);
//swap2(a, b);
swap3(a, b);
cout << "a =" << a << ", b = " << b << endl;
return 0;
}
引用型参数有可能意外修改实参的值,如果不希望修改实参本身,可以将形参定义为常引用,提高传参 效率的同时还可以接收常量型的实参
#include<iostream>
using namespace std;
class Student
{
public:
Student(const string& name, int age) : m_name(name), m_age(age){}
public:
string m_name;
int m_age;
};
void print(const Student& s) //常引用
{
cout << s.m_name << ',' << s.m_age << endl;
//s.age++;//不允许通过常引用修改变量的值
}
int main()
{
/*const*/ Student student("蔡徐坤", 18);
print(student);
return 0;
}
(3)引用型函数返回值
可以将函数的返回值声明为引用,避免返回值所带来的内存开销。
不要从函数中返回局部变量的引用,因为所引用的变量内存会在函数返回以后被释放,但是可以返回 成员变量,静态变量以及全局变量的引用
#include<iostream>
using namespace std;
class A
{
public:
A(int data = 0) : m_data(data){}
int& getValue(void)
{
return m_data;
}
//不能返回局部变量的引用
int& fun(void)
{
int a = 123;
return a;
}
private:
int m_data;
};
int main()
{
A a(100);
//cout << a.data << endl;//100
int b = a.getValue();
cout << b << endl;//100
int& c = a.fun();
cout << c << endl;//VS不会报错 g++报错
return 0;
}
如果一个函数返回值类型被声明为普通引用,那么该函数返回值是一个左值(函数返回值本来是右 值)
#include<iostream>
using namespace std;
class A
{
public:
A(int data = 0) : m_data(data){}
int& getValue(void)
{
return m_data;
}
private:
int m_data;
};
int main()
{
A a(100);
//cout << a.data << endl;//100
cout << (a.getValue())++ << endl;//100
//cout << (a.getValue1())++ << endl;//100
return 0;
}
如果不希望函数直接返回左值,可以返回常引用
#include<iostream>
using namespace std;
class A
{
public:
A(int data = 0) : m_data(data){}
int& getValue(void)
{
return m_data;
}
const int getValue1(void)
{
return m_data;
}
private:
int m_data;
};
int main()
{
A a(100);
//cout << a.data << endl;//100
cout << (a.getValue())++ << endl;//100
//cout << (a.getValue())++ << endl;//error 右值不能++
cout << a.getValue1() << endl;//101
return 0;
}
(4)使用引用的时机(函数的参数和返回值):
如果是基本类型,使用值传递或者指针
如果是数组,只能以指针方式传递
如果是结构体类型,指针、引用都可
如果是类/对象,引用
(5)引用的注意事项
- 避免返回局部变量的引用: 不要返回函数内部局部变量的引用,因为局部变量的生命周期在函数结 束时结束,返回对应的引用会导致悬空引用。
- 确保引用指向有效的内存: 确保引用在其生命周期内始终指向有效的内存,避免使用悬空引用。
- 引用作为函数返回值时的生命周期: 当函数返回引用时,确保返回的引用指向的对象在函数调用后 仍然有效。
- 避免引用和指针混淆: 引用和指针是不同的概念,虽然它们都可以用于访问内存,但它们有着不同 的语法和语义。避免混淆引用和指针的用法。
- 避免滥用引用: 引用是一种强大的工具,但滥用它可能导致代码的可读性和维护性下降。在确实需 要引用的情况下使用它,而不是为了避免传值而过度使用引用。
4.指针在实际编程中的使用
(1)野指针和空指针
指针没有初始化里面是一个垃圾值,称为野指针。程序里不可以出现野指针。
int *p;//野指针
把指针初始化为NULL,即为空指针
int *p = NULL;//推荐
int *q = 0;
(2)指针的大小
无论什么类型的指针得到的总是:4或8 在32位平台,
所有的指针(地址)都是32位(4字节) 在64位平台
所有的指针(地址)都是64位(8字节)
char ch = 'b';
int *a = NULL;
char *b = &ch;
float *c;
std::cout << "sizeof(a) = " << sizeof(a) << std::endl;
std::cout << "sizeof(b) = " << sizeof(b) << std::endl;
std::cout << "sizeof(c) = " << sizeof(c) << std::endl;
(3)通过指针修改变量的值
访问指针指向的存储空间
C++中提供了*来定义指针变量和访问指针变量指向的内存存储空间
在定义变量的时候 * 是一个类型说明符,说明定义的这个变量是一个指针变量
定义完指针变量后,再在指针变量前加*,表示对指针变量解引用,即获取指针变量所指向区 域的内容
int a = 5;
int *p = &a;//此处的*表示p为一个指针
std::cout << "*p = " << *p << std::endl;// 此处的*表示访问指针指向的存储空间(解引用)
通过指针变量修改指针指向的存储空间
int a = 1;
int *pa = &a;
*a = 3;
std::cout << "*pa = " << *pa << std::endl;
std::cout << "a = " << a << std::endl;
野指针和空指针不能解引用(因为没有绑定内存)
但是可以重新绑定内存再解引用
int* p;//野指针
//*p = 1;
std::cout << *p << std::endl;
int* p1 = NULL;
*p1 = 1;
std::cout << *p1 << std::endl;
int a = 1;
p = &a;
std::cout << *p << std::endl;
p1 = &a;
std::cout << *p1 << std::endl;
(4)const修饰的指针变量
声明指针变量的时候可以使用const关键字
常量指针:声明指针变量的时候可以把const关键字写在类型名称前,不可以通过这种指针对捆绑存 储区做赋值,但是可以对这种指针本身做赋值(不可以通过指针对存储区赋值,但可以对指针的地址赋 值)
int a = 1;
int b = 2;
const int *pa = &a;//与int const *pa = &a;效果一样
*pa = 3;//error
a = 3;//right
pa = &b;//right
指针常量:声明指针变量的时候可以把const关键字写在指针变量名称前,可以通过这种指针对捆绑 存储区做赋值,但是不可以对这种指针本身做赋值(可以通过指针对存储区赋值,但不可以对指针的地 址赋值)
int a = 1;
int b = 2;
int * const pa = &a;
*pa = 3//right
pa = &b;//error
(5)数组指针
数组指针
一个变量有地址,一个数组包含若干元素,每个数组元素也有相应的地址, 指针变量也可以保存数 组元素的地址。
只要一个指针变量保存了数组元素的地址, 我们就称之为数组元素指针,即数组指针,数组指 针的本质是指针,指向数组中的某个元素的地址。
数组指针的本质是一个指针。
#include <iostream>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int* pa = a;
int* pb = &a[0];
std::cout << "a = " << a << std::endl;
std::cout << "pa = " << pa << std::endl;
std::cout << "pb = " << pb << std::endl;
return 0;
}//三者一致
(6)数组指针操作数组元素
由于数组名可以代表数组收元素地址,数组元素是可以通过 数组名[下标] 的格式访问,那么可以定 义一个指针来存放数组的地址,并通过 指针[下标] 的方式去访问数组元素。
#include <iostream>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int* pa = a;
*pa = 10;
std::cout << "a[0] = " << a[0] << std::endl;//10
std::cout << "pa[0] = " << pa[0] << std::endl;//10
return 0;
}
(7)数组指针加减运算
在指针指向数组元素时,允许以下运算:
加一个整数(用+或+=),如p+1
减一个整数(用-或-=),如p-1
自加运算,如p++,++p
自减运算,如p–,--p
地址加减整数n实际上加减的是加减n个数组元素的大小。
数组名不能参与加减计算
#include <iostream>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7 };
int* pa = a;
double a1[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
double* pa1 = a1;
std::cout << "pa = " << pa << std::endl;
std::cout << "pa1 = " << pa1 << std::endl;
std::cout << std::endl;
pa++;
pa1++;
//a++;//error 数组名不能参与加减计算
std::cout << "pa = " << pa << std::endl;
std::cout << "pa1 = " << pa1 << std::endl;
std::cout << std::endl;
pa += 2;
//a += 2;//error
std::cout << "pa = " << pa << std::endl;
std::cout << std::endl;
std::cout << "*(pa + 1) = " << *(pa + 1) << std::endl;
std::cout << "*pa = " << *pa << std::endl;
std::cout << "*(pa - 1) = " << *(pa - 1) << std::endl;
std::cout << "*pa - 1 = " << *pa - 1 << std::endl;//这个不是数组元素
std::cout << std::endl;
return 0;
}
通过数组指针遍历数组
#include <iostream>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
for (i = 0; i < n; i++)
{
std::cout << a[i] << " ";
}
std::cout << std::endl;
for (i = 0; i < n; i++)
{
std::cout << *(a + i) << " ";//数组名+偏移量访问
}
std::cout << std::endl;
int* p = a;
//这将使指针向前移动i个位置,然后返回新的地址。
//这并不直接访问数组的特定元素,而是返回一个新地址,该地址是原始地址加上i个元素的大小。
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
std::cout << *(p + i) << " ";//指针法
}
std::cout << std::endl;
//这是通过指针和下标索引访问数组元素的标准方法。
//在编译时,编译器知道数组的大小和类型,因此可以正确地计算出访问特定元素所需的内存地址。
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
std::cout << p[i] << " ";//指针法
}
std::cout << std::endl;
//for (i = 0; i < n; i++, a++)//error 数组名是一个存储了数组首地址的常量,因此不能++
//{
// std::cout << *a;
//}
//std::cout << std::endl;
return 0;
}
(8)函数指针
函数作为一段程序,在内存中也要占据部分存储空间,它也有一个起始地址
&不是必须的,因为函数名就表示了它的地址
函数有自己的地址,指针变量就是用来存储地址的。因此可以利用一个指针指向一个函数。其中,函数 名就代表着函数的地址。
函数指针的定义
格式:返回值类型 (* 指针变量名)(形参1, 形参2...)
= NULL; = NULL 可省略
#include <iostream>
int sum(int a, int b)
{
return a + b;
}
int main()
{
int (*p)(int, int) = NULL;//函数指针声明
p = sum;//函数指针初始化
int a = p(3, 5);//调用
std::cout << "a = " << a << std::endl;//8
return 0;
}
5.指针和引用的区别
相同点:
如果从C的角度来看,其本质就是指针
- 引用和指针都允许对其他变量进行间接访问,通过引用和指针可以修改或获取其他变量的值
- 两者都可以用于函数参数传递,允许在函数内修改调用者传递的变量
- 但是在C++中建议使用引用,而不是指针
不同点:
指针可以不做初始化,其目标可以随便改变(指针常量除外),而引用必须初始化,而且引用目标不能被改变