乐趣区

关于c:C-语言指针详解

1 为什么应用指针

如果咱们定义了 char a=’A’,当须要应用 ‘A’ 时,除了间接调用变量 a,还能够定义 char *p=&a,调用 a 的地址,即指向 a 的指针 p,变量 achar 类型)只占了一个字节, 指针自身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。

但如果要援用的是占用内存空间比拟大货色,用指针也还是 4 个字节即可。

  • 应用指针型变量在很多时候占用更小的内存空间。

变量为了示意数据,指针能够更好的传递数据,举个例子:

第一节课是 1 班语文,2 班数学,第二节课颠倒过去,1 班要上数学,2 班要上语文,那么第一节课下课后须要怎么作调整呢?计划一:课间 1 班学生全都去 2 班,2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……局面一片狼藉;计划二:两位老师课间调换教室。

显然,计划二更好一些,计划二相似应用指针传递地址,计划一将内存中的内容从新“复制”了一份,效率比拟低。

  • 在数据传递时,如果数据块较大,能够应用指针传递地址而不是理论数据,即进步传输速度,又节俭大量内存。

一个数据缓冲区 char buf[100],如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int,应用如下语句进行赋值:

*(short*)&buf[0]=DataId;
*(short*)&buf[2]=DataType;
*(int*)&buf[4]=DataValue;
  • 数据转换,利用指针的灵便的类型转换,能够用来做数据类型转换,比拟罕用于通信缓冲区的填充。
  • 指针的机制比较简单,其性能能够被集中从新实现成更抽象化的援用数据模式
  • 函数指针,形如: #define PMYFUN (void*)(int,int),能够用在大量分支解决的实例当中,如某通信依据不同的命令号执行不同类型的命令,则能够建设一个函数指针数组,进行散转。
  • 在数据结构中,链表、树、图等大量的利用都离不开指针。

2 指针是什么?

操作系统将硬件和软件联合起来,给程序员提供的一种对内存应用的形象,这种形象机制使得程序应用的是虚拟存储器, 而不是间接操作和应用实在存在的物理存储器。所有的虚拟地址造成的汇合就是虚拟地址空间。

内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有惟一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范畴就是 0 ~ 4294967296,十六进制示意就是 0x00000000~0xffffffff,当程序应用的数据载入内存时,都有本人惟一的一个编号,这个编号就是这个数据的地址。指针就是这样造成的。

#include <stdio.h>

int main(void)
{
 char ch = 'a';
 int num = 97;
 printf("ch 的地址:%pn",&ch); 
 //ch 的地址:00BEFDF7
 printf("num 的地址:%pn",&num); 
 //num 的地址:00BEFDF8
 return 0;
}

指针不仅能够示意变量的地址,还能够存储各种类型数据的地址,指针变量是用来保留这些地址的变量,与数组相似,根据地址寄存的数据类型,指针也分为 int 指针类型,double 指针类型,char 指针类型等等。

综上,指针的本质就是数据在内存中的地址,而指针变量是用来保留这些地址的变量

指针变量 和 指向关系

用来保留 指针 的变量,就是指针变量。如果指针变量 p 保留了变量 num的地址,则就说:p指向了变量 num,也能够说p 指向了 num 所在的内存块,指针变量 pp 指向了 p 所在的内存块,以上面为例:

#include <stdio.h>

int main(void)
{
 int num = 97;
 char ch = 'a';
 int *p = & num;
 int **pp = &p;
 char *p1 = & ch;
 printf("num 的地址:%pn",&num); 
 printf("指针 p 的值:%pn",p); 
 printf("指针 p 的地址:%pn",&p); 
 printf("指针 pp 的值:%pn",pp); 
 printf("ch 的地址:%pn",&ch); 
 return 0;
}

运行后果

  • int 型的 num 值为 97 占 4 个字节,内存地址为:0113F924char 型的 ch('a') 值为 97 占 1 个字节,内存地址为:0113F91B

int 型占 4 个字节

char 型占 1 个字节

  • num的地址为:0113F924num的值为 97,指针 p 指向 num 的内存块,指针 p 地址为:0113F90Cp的内存保留的值就是 num 的地址0113F924

0x0113F90C 存储的内容为地址 0113F924

  • 指针变量 pp 指向 指针 p,指针 pp 内存值为 指针 p 的地址:0113F90C,造成了只想指针的指针。

指针 pp 为指向指针 p 的指针

定义指针变量

C 语言中,定义变量时,在变量名 前 写一个 * 星号,这个变量就变成了对应变量类型的指针变量。必要时要加() 来防止优先级的问题。

引申:C 语言中,定义变量时,在定义的最后面写上 typedef,那么这个变量名就成了一种类型,即这个类型的同义词。

int a ; //int 类型变量 a
int *a ; //int* 变量 a
int arr[3]; //arr 是蕴含 3 个 int 元素的数组
int (* arr)[3]; //arr 是一个指向蕴含 3 个 int 元素的数组的指针变量
int* p_int; // 指向 int 类型变量的指针 
double* p_double; // 指向 idouble 类型变量的指针 
struct Student *p_struct; // 构造体类型的指针
int(*p_func)(int,int); // 指向返回类型为 int,有 2 个 int 形参的函数的指针 
int(*p_arr)[3]; // 指向含有 3 个 int 元素的数组的指针 
int** p_pointer; // 指向 一个整形变量指针的指针

取地址

既然有了指针变量,那就得让他保留其它变量的地址,应用 & 运算符获得一个变量的地址。

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 = &num;
 float* p_score = &score;
 int (*p_arr)[3] = &arr; 
 int (*fp_add)(int ,int)  = add; //p_add 是指向函数 add 的函数指针
 return 0;
}

非凡的状况,他们并不一定须要应用 & 取地址

  • 数组名的值就是这个数组的第一个元素的地址。
  • 函数名的值就是这个函数的地址。
  • 字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称, 也就是这个字符串在内存中的地址。
int add(int a , int b){return a + b;}

int main(void)
{int arr[3] = {1,2,3};
 int* p_first = arr;
 int (*fp_add)(int ,int)  =  add;
 const char* msg = "Hello world";
 return 0;
}

解地址

对一个指针解地址,就能够取到这个内存数据,解地址 的写法,就是在指针的后面加一个 * 号。

解指针的本质是:从指针指向的内存块中取出这个内存数据。

int main(void)
{
 int age = 19;
 int*p_age = &age;
 *p_age  = 20; // 通过指针批改指向的内存数据
 printf("age = %d",*p_age); // 通过指针读取指向的内存数据
 printf("age = %d",age);
 return 0;
}

空指针

空指针在概念上不同于未初始化的指针。空指针能够确保不指向任何对象或函数;而未初始化的指针则可能指向任何中央。空指针不是野指针。

在 C 语言中,咱们让指针变量赋值为 NULL 示意一个空指针,而 C 语言中,NULL 本质是 ((void*)0),在 C ++ 中,NULL 本质是 0。

#ifdef __cplusplus
 #define NULL    0
#else 
 #define NULL    ((void *)0)
#endif

void* 类型指针

void_是一种非凡的指针类型,能够用来寄存任意对象的地址。一个 void_指针寄存着一个地址,这一点和其余指针相似。不同的是,咱们对它到底贮存的是什么对象的地址并不理解。

double a=2.3;
int b=5;
void *p=&a;
cout<<p<<endl; // 输入了 a 的地址
p=&b;

cout<<p<<endl; // 输入了 b 的地址

//cout<<*p<<endl; 这一行不能够执行,void* 指针只能够贮存变量地址,不能够间接操作它指向的对象

因为 void 是空类型,只保留了指针的值,而失落了类型信息,咱们不晓得他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要残缺的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,而后再解指针。

数组和指针

  • 同类型指针变量能够互相赋值,数组不行,只能一个一个元素的赋值或拷贝
  • 数组在内存中是间断寄存的,开拓一块间断的内存空间。数组是依据数组的下进行拜访的。指针很灵便,它能够指向任意类型的数据。指针的类型阐明了它所指向地址空间的内存。
  • 数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在 32 位平台下,无论指针的类型是什么,sizeof(指针名)都是 4,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8。
  • 数组名作为右值的时候,就是第一个元素的地址
int main(void)

{int arr[5] = {1,2,3,4,5};
 int *p_first = arr;
 printf("%d",*p_first); //1
 return 0;
}
  • 指向数组元素的指针 反对 递增 递加 运算。p= p+1意思是,让 p 指向原来指向的内存块的下一个相邻的雷同类型的内存块。在数组中相邻内存就是相邻下标元素。

函数与指针

函数的参数和指针

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

void change(int a)
{a++; // 在函数中扭转的只是这个函数的局部变量 a,而随着函数执行完结,a 被销毁。age 还是原来的 age,岿然不动。}

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

有时候咱们能够应用函数的返回值来回传数据,在简略的状况下是能够的,然而如果返回值有其它用处(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

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

void change(int* pa)
{(*pa)++; // 因为传递的是 age 的地址,因而 pa 指向内存数据 age。当在函数中对指针 pa 解地址时,// 会间接去内存中找到 age 这个数据,而后把它增 1。}

int main(void)
{
 int age = 160;
 change(&age);
 printf("age = %d",age); // age = 61
 return 0;
}

比方指针的一个常见的应用例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap(int *,int *);

int main()
{
 int a=5,b=10;
 printf("a=%d,b=%dn",a,b);
 swap(&a,&b);
 printf("a=%d,b=%dn",a,b);
 return 0;
}

void swap(int *pa,int *pb)
{int t=*pa;*pa=*pb;*pb=t;}

在以上的例子中,swap函数的两个形参 pa 和 pb 能够接管两个整型变量的地址,并通过间接拜访的形式批改了它指向变量的值。在 main 函数中调用 swap 时,提供的实参别离为 &a,&b,这样就实现了pa=&a,pb=&b 的赋值过程,这样在 swap 函数中就通过 *pa 批改了 a 的值,通过 *pb 批改了 b 的值。因而,如果须要在被调函数中批改主调函数中变量的值,就须要通过以下几个步骤:

  • 定义函数的形参必须为指针类型,以接管主调函数中传来的变量的地址;
  • 调用函数时实参为变量的地址;
  • 在被调函数中应用 * 间接拜访形参指向的内存空间,实现批改主调函数中变量值的性能。

指针作为函数的形参的另一个典型利用是当函数有多个返回值的情景。比方,须要在一个函数中统计一个数组的最大值、最小值和平均值。当然你能够编写三个函数别离实现统计三个值的性能。但比拟啰嗦,如:

int GetMax(int a[],int n)

{int max=a[0],i;
 
 for(i=1;i<n;i++)
 {if(max<a[i]) max=a[i];
 }
 
 return max;
}

int GetMin(int a[],int n)
{int min=a[0],i;
 
 for(i=1;i<n;i++)
 {if(min>a[i]) min=a[i];
 }
 
 return min;
}

double GetAvg(int a[],int n)
{
 double avg=0;
 int i;
 
 for(i=0;i<n;i++)
 {avg+=a[i];
 }

 return avg/n;
}

其实咱们齐全能够在一个函数中实现这个性能,因为函数只能有一个返回值,能够返回平均值,最大值和最小值能够通过指针类型的形参来进行实现:

double Stat(int a[],int n,int *pmax,int *pmin)
{double avg=a[0];
 int i;
 *pmax=*pmin=a[0];
 
 for(i=1;i<n;i++)
 {avg+=a[i];
 if(*pmax<a[i]) *pmax=a[i];
 if(*pmin>a[i]) *pmin=a[i];
 }

 return avg/n;
}

函数的指针

一个函数总是占用一段间断的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址。咱们能够把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的内存区域,而后通过指针变量就能够找到并调用该函数。这种指针就是函数指针。

函数指针的定义模式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中能够同时给出参数的类型和名称,也能够只给出参数的类型,省略参数的名称,这一点和函数原型十分相似。

用指针来实现对函数的调用:

#include <stdio.h>
// 返回两个数中较大的一个

int max(int a, int b)
{return a>b ? a : b;}

int main()
{
 int x, y, maxval;
 // 定义函数指针
 int (*pmax)(int, int) = max; // 也能够写作 int (*pmax)(int a, int b)
 printf("Input two numbers:");
 scanf("%d %d", &x, &y);
 maxval = (*pmax)(x, y);
 printf("Max value: %dn", maxval);

 return 0;
}

构造体和指针

构造体指针有非凡的语法:-> 符号

如果 p 是一个构造体指针,则能够应用 p ->【成员】的办法拜访构造体的成员

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

int main(void)
{Student stu = {"Bob" , 19, 98.0};
 Student*ps = &stu;
 ps->age = 20;
 ps->score = 99.0;
 printf("name:%s age:%d",ps->name,ps->age);
 return 0;
}

const 和 指针

  • 指向常量的指针,值不能扭转,指向可扭转
  • 常指针值能扭转,指向不可扭转
  • 指向常量的常指针,都不能扭转
#include <stdio.h>

int main()
{

 // 1 可扭转指针
 const int a = 10;
 int *p = &a;
 *p = 1000;
 printf("*p = %dn", *p);

 // 2 可扭转指针
 const b = 10;
 int *pb = &b;
 pb = p;
 printf("*pb = %dn", *pb);

 // 3
 const c = 10;
 int * const pc = &c;
 *pc = 1000;
 //pc = pb; 不能扭转

 //4
 const d = 10;
 const * int const pd = &d;
 //*pd = 1000; 不能扭转
 printf("n");

 return 0;
}

深拷贝和浅拷贝

如果 2 个程序单元(例如 2 个函数)是通过拷贝 他们所共享的数据的 指针来工作的,这就是浅拷贝,因为真正要拜访的数据并没有被拷贝。如果被拜访的数据被拷贝了,在每个单元中都有本人的一份,对指标数据的操作互相 不受影响,则叫做深拷贝。

#include <iostream>
using namespace std;

class CopyDemo
{
public:
 CopyDemo(int pa,char *cstr) // 构造函数,两个参数
 {
 this->a = pa;
 this->str = new char[1024]; // 指针数组,动静的用 new 在堆上调配存储空间
 strcpy(this->str,cstr); // 拷贝过去
 }

// 没写,C++ 会主动帮忙写一个复制构造函数,浅拷贝只复制指针, 如下正文局部
 //CopyDemo(CopyDemo& obj) 
 //{
 //   this->a = obj.a;
 //  this->str = obj.str; // 这里是浅复制会出问题,要深复制
 //}

 CopyDemo(CopyDemo& obj) // 个别数据成员有指针要本人写复制构造函数,如下
 {
 this->a = obj.a;
 // this->str = obj.str; // 这里是浅复制会出问题,要深复制
 this->str = new char[1024];// 应该这样写
 if(str != 0)
 strcpy(this->str,obj.str); // 如果胜利,把内容复制过去
 }

 ~CopyDemo() // 析构函数
 {delete str;}

public:
 int a; // 定义一个整型的数据成员
 char *str; // 字符串指针
};

int main()
{CopyDemo A(100,"hello!!!");
 CopyDemo B = A; // 复制构造函数,把 A 的 10 和 hello!!! 复制给 B
 cout <<"A:"<< A.a << "," <<A.str << endl;
 // 输入 A:100,hello!!!
 cout <<"B:"<< B.a << "," <<B.str << endl;
 // 输入 B:100,hello!!!
 // 批改后, 发现 A,B 都被扭转,起因就是浅复制,A,B 指针指向同一中央,批改后都扭转

 B.a = 80;
 B.str[0] = 'k';
 cout <<"A:"<< A.a << "," <<A.str << endl;
 // 输入 A:100,kello!!!
 cout <<"B:"<< B.a << "," <<B.str << endl;
 // 输入 B:80,kello!!!
 
 return 0;
}

依据下面实例能够看到,浅复制仅复制对象自身(其中包含是指针的成员),这样不同被复制对象的成员中的对应非空指针会指向同一对象,被成员指针援用的对象成为共享的,无奈间接通过指针成员平安地删除(因为若间接删除,另外对象中的指针就会有效,造成所谓的野指针,而拜访有效指针是危险的;

除非这些指针有援用计数或者其它伎俩确保被指对象的所有权);而深复制在浅复制的根底上,连同指针指向的对象也一起复制,代价比拟高,然而绝对容易治理。

参考资料

C Primer Plus(第五版)中文版

https://www.cnblogs.com/lulipro/p/7460206.html

退出移动版