C++基础:指针&引用

C++指针

数据存储在内存中,但并不是乱存的,每一个进程(程序)都有属于自己的内存空间,每一个变量都有属于自己的内存位置,地址即表示内存存储的位置的量,对变量的读写操作本质是对相应内存存储区域的存取操作。每一个内存位置都定义了可以使用连字号‘&’运算符访问的地址。
指针本身是一个变量,其数据类型是“地址型”,即里面存储的只能是地址值,通常是存储的是另一个变量的地址,即内存位置的直接地址。
指针变量定义的一般形式:

type *var_name;		//所指向的存储空间的存储的数据类型 * 指针变量名

值得注意的是指针变量定义前的数据类型指的并不是指针变量的数据类型,而是指针变量指向的内存空间里存储的数据的数据类型,星号‘*’可以理解为是定义指针变量的关键字,用来告知编译器该变量是指针变量,在一个系统中,指针的数据大小是固定的,所有指针的实际数据类型都是一样的,指针长度取决于系统的寻址能力,32位机4字节,64位机8字节,指针的长度和它指向的数据类型无关。但不同数据类型的指针表示其指向的变量或常量的数据类型不同。且不同数据类型的指针无法进行赋值操作。
指针定义时需要指明指向的数据的数据类型,是为了方便寻址和地址计算。

char arr1[10]="hello";
char *var1;//指针变量的数据类型和其指向的数据的数据类型必须一致
var1=arr1;//数组名即是数组首地址,即数组名本身是一个常量指针,因此可以赋值。
cout<<*var1<<endl;//var1指向字符数组的第一个元素,因此对指针var1解地址操作,得到的是字符h
cout<<*(var1+2)<<endl;//var1+2即将地址指向偏移两个单位,单位对应于指针变量的数据类型,此处是char型,一个字节,因此var1+2即在指向第一个元素的地址的基础上向右偏移两个字节的长度,此时指向的是字符数组的第三个数据,对应arr1[2],即字符l。
int arr2[10]={1,2,3,4,5,6};
int *var2;
var2=arr2;
cout<<*var2<<endl;
cout<<*(var2+2)<<endl;//同样,偏移量是两个单位,但此时,由于var2指针变量数据类型是int型,因此偏移量是一个单位对应4字节,新的地址为var2+2*sizeof(int)。同样指向的是arr2[2]即数字3。
//即:指针变量的数据类型规定了指针操作的偏移单位。

可以这样理解:
我们通过*号来定义指针变量var2。将这两者看作一个整体,那么就变成了( *var2 )是一个int型数据变量,这就是为何星号‘ * ’在定义变量时代表指针变量的定义,在调用时就意味着解地址数据(注意:指针变量的定义只是开辟出了存放地址数据的指针内存空间,它并没有开辟出指向的数据的内存空间)。那么反观这个数据是int型的,因此数据占据4字节内存空间,程序运行时,数据作为访问的基本单位,因为对于int型,按字节访问是没有意义的,那么var2+1实际上就是从访问第一个数据到访问第二个数据。这就是为何在地址定义时必须声明指向的数据类型,它规定了地址计算单位。

空指针

在指针变量声明时,如果没有却确的地址赋值,通常初始化为NULL,这是一种编程习惯,同样也可以在后面调用时以此来确定指针是否为空。赋值为NULL的指针称为空指针,NULL 指针是一个定义在标准库中的值为零的常量。即它指向起始地址为0x00000000的空间,但大多数操作系统上,程序是不允许访问地址为0x00000000的内存空间的,通常是操作系统自己保留的内存段,因此,如果指针指向空则意味着指向的是不可访问的内存,即指针指向非法,因此可以以此判定指针的有效性。
如果所有未使用的指针都被初始化赋予空值,同时避免使用空指针,就可以防止误用一个未初始化的指针。
空指针的特殊用法:计算结构体成员偏移量

#define OFFSET(TYPE,MEMBER)  ( (size_t)( &( ( (TYPE*)0 )->MEMBER ) ) )

由于结构体对齐的存在,在定义结构体成员时,其成员间的地址偏移并不会按照成员变量的数据类型来偏移,但很多时候,在没有定义数据之前需要提前知道其地址偏移量,因此需要用NULL指针来实现这一计算。(TYPE*)0即定义一个结构体指针,指向NULL; ( (TYPE*)0 )->MEMBER 即是访问结构体成员,原则上不允许访问空指针的成员的;&( ( (TYPE*)0 )->MEMBER ) )取址符即不访问成员变量,只获取变量的地址,由于起始地址是NULL,因此此时的成员地址就是成员的偏移量。

指针的算术运算

指针支持加减法运算和递增递减运算,对指针的递增递减运算,不会影响内存中的数据,只是将指针移到下一个数据的内存的位置,指针的移动单位是指针的数据类型大小,即对于char型指针移动单位为一个字节,int型指针移动单位为4字节。
在程序中常常使用指针代替数组,因为变量指针可以递增,而数组不能递增,因为数组名是一个指针常量。

//指针实现遍历
#include<iostream>
using namespace std;
const int MAX = 10;
int main(){
	int arr[MAX]={1,2,3,4,5,6,7};
	int *ptr;
	ptr=arr;
	for(int i=0;i<MAX;i++){
		cout<<"address : "<<ptr<<" \t Value :"(*ptr)<<endl;
		ptr++;
	}
}
//也可以通过递减方式实现遍历

指针也支持关系运算符的比较运算:>、<、==、!=、>=、<=。在容器遍历时经常以判定是否是最后一个元素地址为条件来更新迭代器。

#include<iostream>
using namespace std;
const int MAX = 10;
int main(){
	int arr[MAX]={1,2,3,4,5,6,7};
	int *ptr;
	ptr=arr;
	if(ptr<=&arr[MAX-1]){
		cout<<"address : "<<ptr<<" \t Value :"(*ptr)<<endl;
		ptr++;
	}
}

数组名:常量指针

多数情况下,使用指针变量来对数组进行操作和使用下标索引是等价的,且指针变量的操作更简洁高效,数组名本身也是一个指针,但不同的是,指针变量的数据即其指向的地址是可变的,但数组名属于指针常量,其指向的地址是不允许改变的。

int arr[10];
*(arr+3)=100;//这个操作并没有改变arr的值,属于合法操作

把指针运算符 * 应用到 arr 上是完全可以的,但修改 arr 的值是非法的。这是因为 arr 是一个指向数组开头的常量,不能作为左值。

指针数组

指针数组:即存储指针数据的数组,其本质是数组,数组元素则是指针数据(地址)。

Type *arr_name[num];

二级指针

指针变量存储的是地址数据,对于一级指针而言,通过解地址即可得到指针指向的内存空间中的数据。二级指针则是指针所指向的数据本身也是一个指针数据。

Type **pptr;

二级指针理解时,从右往左理解,首先 pptr 它是一个变量,右边的 ‘ * ’ 号表示变量存储的是地址数据,左边的 ‘ * ’ 号表示该地址数据所代表的内存空间中存储的依然是地址数据,Type表示二级地址最终指向的数据类型。
二级指针多用于函数传参,即实参本身是一个指针数组时,传递给函数则需要二级指针。

指针传参和返参

当函数的参数是数组或者数据量比较大的数据时,为了避免函数调用时进行内存空间的开辟,通常选择进行指针传递;同样,函数产生的数据量比较大时,函数返回的也可以是地址,而不是数据本身,此时函数的数据通常定义为static类型或者将内存开辟在堆区,因为函数返回时,局部变量占据的内存空间将被释放回收。

Type * myFunction(Type * ptr){}

指针常量和常量指针

判定是指针常量还是常量指针的方式是看const和星号‘*’的位置,从左往右读,星号在前即是指针常量,const在前面则是常量指针。
理解指针常量和常量指针的方法是只看后面的词,即文字意义上,两个词放在一起,通常第一个词起修饰作用,第二个词才是主体。
指针常量则表示ptr本身是一个常量,又有ptr是指针,存储的是地址数据,则指针常量表示指针指向的地址不能发生改变,但该地址所代表的内存空间里的数据可以改变。
常量指针表示ptr本身是一个指针,存储的是地址数据,且这个地址所代表的内存空间里的数据不允许改变。但是,指针变量里存储的地址数据是可以改变的。

int * const ptr;		//指针常量
const int *ptr;			//常量指针

另一种理解:将变量名和离它最近的符号括起来,把其它部分括起来

(int *)( const ptr);			//指针常量		ptr被const修饰,则ptr的数据不可以改变
(const int)( *ptr);			//常量指针		int被const修饰,则ptr指向的内存空间存储的是常量数据

指针常量用于避免出现野指针,而常量指针用于进行数据的写保护,避免误操作。

指针函数和函数指针

指针函数本质是一个函数,返回的数据类型是指针型;函数指针本质是一个指针,指针指向的是一个函数地址。

Type * Function();			//指针函数:返回值是指针数据
Type (*fun)();				//函数指针:可以对变量fun进行赋值,以指向一个函数

指针函数:

#include<iostream>
using namespace std;
int *my_fun(int x){
	static int c=0;
	c+=x;
	return &c;
}
int main(){
	int a=10;
	int *ptr;
	ptr=my_fun(a);
	cout<<"*ptr = "<<*ptr<<endl;
	return 0;
}

函数指针:

#include<iostream>
using namespace std;

int my_add(int x,int y){
	return x+y;
}
int (*fun)(int,int);//指针函数定义声明,也可以在main函数内部,不同在于作用域
int main(){
	fun=&my_add;
	//fun=my_add;//函数名本身也是一个指针常量。
	int a=10;
	int b=20;
	cout<<my_add(a,b)<<endl;
	cout<<(*fun)(a,b)<<endl;//不同于函数名,函数指针必须解地址调用函数
	return 0;
}

C++引用

引用的本质就是起别名,引用变量就是给一个已经存在的变量起了另一个名字,操作引用变量和操作原来的变量效果相同。
引用和指针的不同:1指针可以为空,引用不能为空,即引用在创建时必须初始化,而指针可以在任何时候初始化;2一旦引用被初始化为一个对象,就不能再改变,即具有常量性质,而指针可以改变其指向。
引用的标识符是&,和取址符相同,不同之处在于在定义变量时使用代表引用,在赋值能操作中使用表示取地址;或者说,&作为左值时是引用,作为右值时是取址

int i=10;
int& r=i;

引用与函数参数和返回值

引用即可以作为函数参数进行传递,也可以作为函数返回值返回
作为函数参数时,相比于值传递,引用传递是使函数获得了对实参的操作权限,没有进行值拷贝;相比于地址传递,引用传递的变量完全和实参变量本身的操作一致,避免了解地址操作等书写的麻烦。

#include<iostream>
using namespace std;
void my_swap(int& x,int& y){
	int z;
	z=x;x=y;y=z
	return;
}

int main(){
	int x=10,y=20;
	cout<<"before : x="<<x<<"  y= "<<y<<endl;
	my_swap(x,y);
	cout<<"after : x="<<x<<"  y= "<<y<<endl;
}

作为返回值时,相比于值传递,引用传递返回的是变量本身,而不是变量里面存储的值,因此引用的返回可以直接作为左值存在,便于链式操作,同时也避免了值拷贝对计算的消耗;相比于地址传递,地址本身也是一个数据,它也无法作为左值存在,而引用可以。

#include<iostream>
using namespace std;
int& my_fun(int m){
	static int n=0;
	n+=m
	cout<<"now n= "<<n<<endl;
	return n;
}

int main(){
	my_fun(10)=20;
	my_fun(20);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值