指针

发布时间 2024-01-06 11:54:43作者: 【SmarT】

指针

指针类型和指针类型的转换

https://www.cnblogs.com/chanabi/p/9228998.html

预备知识复习:

1. 地址,字节和位:

位(bit)是计算机最小的数据单位,每一位的状态只能是0或1;

字节(Byte)是用于计算存储容量的一种单位,每一个字节由8位组成(1Byte = 8bit);

地址可以理解为在一片内存中每个字节的编号。

关系比喻:内存是一栋大楼,字节(Byte)是大楼中的每一层,地址是楼层编号,位(bit)是每一层中的房间,每一层有8个房间。

2. 变量的内存:

编译器根据变量的类型,在内存中申请一块空间。例如32位与64位中 int 类型申请到4字节的空间,可理解为编译器申请了4层楼,作为”办公区域“。

3. 指针变量:

指针是指程序数据在内存中的地址。在c语言当中,允许用一个变量来存放指针,这种变量称为指针变量。

指针变量的作用

int a;
int *p;
p = &a;
printf("%p %d\n",p,*p);

以上程序中,”&“操作符出了变量 a 在内存空间中的首地址,而后通过 “ * ” 操作符取出首地址所在内存空间的数据

不要混淆 取地址 和 引用,当&说明符前面带有类型声明,则是引用,否则就是取地址。通俗来说 &在 ”=” 号左边的是引用,右边的是取地址。

在声明一个指针变量的时候,会根据它将要指向的变量类型,声明对应的类型,例如:

int a;
long b;
char c;

int *pa = &a;
long *pb = &b;
char *pc = &c;

指针的值实质是内存单元(即字节)的编号,不管是什么类型的指针变量,所存的值都是地址(int类型的值)。

声明不同类型的作用是什么?答案是规定指针在内存中每次移动的字节数。

4. “值相同的两个指针变量”

意思是两个指针变量指向同一个首地址。但是如果指针变量的类型不同,因为指针移动的字节数量不同,就可能读取出不同的数据。

  1. 要实现不同类型指针变量指向同一个地址,需要使用指针类型转换。

    short a = 1;
    short *p1 = &a;
    int *p2 = (int *)p1;
    printf("%d %d",*p1,*p2);
    

    以上例子将一个每次移动读取2字节的 short 类型指针变量,转化为一个每次读取4字节的int型指针变量。

https://blog.csdn.net/weixin_39640298/article/details/84900326

5. 取地址符&

指针用于存放某个对象的地址,想要获取该地址,需使用取地址符&,如下:

int add(int a , int b)
{
    return a + b;
}

int main(void)
{
    int num = 97;
    float score = 10.00F;
    int arr[3] = {1,2,3};
    
    int* p_num = #
    int* p_arr1 = arr;		//p_arr1意思是指向数组第一个元素的指针
    float* p_score = &score;
    int (*p_arr)[3] = &arr;           
    int (*fp_add)(int ,int )  = add;  //p_add是指向函数add的函数指针
    const char* p_msg = "Hello world";//p_msg是指向字符数组的指针
    return 0;
}

通过上面可以看到&的使用,但是有几个例子没有使用&,因为这是特殊情况:

  • 数组名的值就是这个数组的第一个元素的地址
  • 函数名的值就是这个函数的地址
  • 字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址
6. 取值符* 解析地址对象:

如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象,如下:

int  age = 19;
int* p_age = &age;
*p_age  = 20;  			//通过指针修改指向的内存数据

printf("age = %d\n",*p_age);   	//通过指针读取指向的内存数据
printf("age = %d\n",age);

对于结构体和类,两者的差别很小,所以几乎可以等同,则使用->符号访问内部成员

struct Student
{
 char name[31];
 int age;
 float score;
};

int main(void)
{
 Student stu = {"Bob" , 19, 98.0};
 Student*	p_s = &stu;

 p_s->age = 20;
 p_s->score = 99.0;
 printf("name:%s age:%d\n",p_s->name,p_s->age);
 return 0;
}
7. 指针值的状态

指针的值(即地址)总会是下列四种状态之一:

  • 指向一个对象的地址
  • 指向紧邻对象所占空间的下一个位置
  • 空指针,意味着指针没有指向任何对象
  • 无效指针(野指针),上述情况之外的其他值

第一种状态很好理解就不说明了;

第二种状态主要用于迭代器和指针的计算;

空指针:在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是 ((void*)0) , 在C++中,NULL实质是0。C++中也可以使用C11标准中的nullpte字面值赋值,意思是一样的。
任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。

无效指针:指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是无效指针,不能对他们做解指针操作,否则程序会出现运行时错误,导致程序意外终止。

任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。坏指针是造成C语言Bug的最频繁的原因之一。

未经初始化的指针就是个无效指针,所以在定义指针变量的时候一定要进行初始化。如果实在是不知道指针的指向,则使用nullptr或NULL进行赋值。

8. 指针之间的赋值

指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。

int* p1  = &a;
int* p2 = p1;
9. 指针内含信息

通过上面的介绍,我们可以看出指针包含两部分信息:所指向的值和类型信息。

指针的值:这个就不说了,上面大部分都是这个介绍。
指针的类型信息:类型信息决定了这个指针指向的内存的字节数并如何解释这些字节信息。一般指针变量的类型要和它指向的数据的类型匹配。

同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据。

char array1[20] = "abcdefghijklmnopqrs";
char *ptr1 = array1;
int* ptr2 = (int*)ptr1;

ptr1++;
ptr2++;
cout << &array1 << endl;
cout << *ptr1 << endl;
cout << ptr2 << endl;
cout << *(char*)ptr2 << endl;
10. void*指针

void* 指针是一种特殊的指针类型,可用于存放任意对象的地址,但是丢失了类型信息。如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。因为,编译器不允许直接对void*类型的指针做解指针操作(提示非法的间接寻址)。

利用void所做的事儿比较有限:拿它和别的指针比较作为函数的输入或输出,或者给另外一个void对象。

11. 指针的算数运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,指针的运算是有单位的。

如上面介绍的例子:
ptr1和ptr1都被初始化为数组的地址,并且后续都加1。
指针ptr1的类型是char,它指向的类型是char,ptr1加1,编译器在1的后面乘上了单位sizeof(char)。
同理ptr2的类型是int
,它指向的类型是int,ptr2加1,编译之后乘上了单位sizeof(int)
所以两者的地址不一样,通过打印信息,可以看出两者差了2个字符。

若ptr2+3,则编译之后的地址应该是 ptr2的地址加 3 * sizeof(int),打印出的字母应该是m

指针运算最终会变为内存地址的元素,内存又是一个连续空间,所以按理只要没有超出内存限制就可以一直增加。这样前面所说的指针值的状态第二条就很好解释了。

函数和指针

1. 函数的参数和指针

实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

void change(int a)
{
    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。
}

int main(void)
{
    int age = 19;
    change(age);
    printf("age = %d\n",age);   // age = 19
    return 0;
}

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

传递变量的指针可以轻松解决上述问题。

void change(int* pa)
{
    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时
               //会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
    int age = 19;
    change(&age);
    printf("age = %d\n",age);   // age = 20
    return 0;
}

上述方法,当然也可以用引用的方式。

传递指针还有另外一个原因:
有时我们会传递类或者结构体对象,而类或者结构体占用的内存有时会比较大,通过值传递的方式会拷贝完整的数据,降低程序的效率。而指针只是固定大小的空间,效率比较高。当然如果你用C++,使用引用效率比指针更高。

2. 函数的指针

每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。

其实函数名单独进行使用时就是这个函数的指针。

int add(int a,int b)		//函数的定义
{
	return a + b;
}

int (*function)(int,int); 	//函数指针的声明

function = add;		//给函数指针赋值
function = &add;		//跟上面是一样的

int c = function(1,2);	 	//跟函数名一样使用
int d = (*function)(1,2);	//跟上面的调用是一样的

3. 返回值和指针

不要把非静态局部变量的地址返回。我们知道局部变量是在栈中的,由系统创建和销毁,返回之后的地址有可能有效也有可能无效,这样会造成bug。

可以返回全局变量、静态的局部变量、动态内存等的地址。

4. const与指针

指针常量和常量指针,两者的区别是看const修饰的谁。

(1)常量指针

实际是个指针,指针本身是个常量。

int	a = 97;
int	b = 98;
int* const p = &a;
*p 	= 98;		//正确
p 	= &b;		//编译出错
12345

常量指针必须初始化,而且一旦初始化完成,则它的值就不能改变了

(2)指向常量的指针

int a = 97;
int b = 98;       
const int* p = &a;
int const *p = &a; 	//两者的含义是一样的
*p = 98;		//编译出错
p = &b;			//正确

所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,但是对象的值可以通过其它途径进行改变。

需要注意的是常量变量,必须使用指向常量的指针来指向。但是对于非常量变量,两者都可以。

深拷贝和浅拷贝

如果2个程序单元(例如2个函数)是通过拷贝他们所共享的数据的指针(指向同一个地址)来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝(在不同的地址各存储该内容)了,在每个单元中都有自己的一份,对目标数据的操作相互 不受影响,则叫做深拷贝

指针和数组

数组被转换为指针后不是lvalue的原因是没有确切地址,而不能被赋值是结果。

引用

1.什么是引用

引用是给对象起了个别名,是一种复合类型。通过声明符写成 &d 的形式来定义引用类型,其中d是声明的变量名。定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因此引用必须初始化,而且一弄不能重新绑定到另外一个对象,类型要和绑定的类型一致。

引用本身不是独立的数据类型,所以不占空间,引用的地址就是对象的地址。也就无法定义指向引用的指针,无法声明存储引用类型的数组等。

2.引用的使用

引用作为函数参数,目的主要用于函数参数的传递中,解决大块数据或对象的传递效率和空间不如意的问题。

用引用传递函数的参数,能保证参数在传递的过程中不产生副本,从而提高传递效率,同时通过const的使用,还可以保证参数在传递过程中的安全性。

引用本身是目标变量或对象的别名,对引用的操作本质上就是对目标变量或对象的操作。因此能使用引用时尽量使用引用而非指针。

没有void类型的引用。

3.左值引用和右值引用

从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。

有一个本广泛认同的说法:

  • 可以取地址的,有名字的,非临时的就是左值;
  • 不能取地址的,没有名字的,临时的就是右值;
左值引用:

左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。

右值引用:

右值引用是C++ 11新增的特性,其定义如下:

类型 && 引用名 = 右值表达式;

在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。

临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。

带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。