由浅入深,带你探索C语言指针的魅力

简介: 由浅入深,带你探索C语言指针的魅力

一、什么是指针?


定义:指针是一种数据类型,用来存放内存地址单元。我们通常把具有指针类型的变量称为指针变量。


首先我们需要知道指针是如何定义的,指针定义格式如图中ptr1变量的定义,其次我们要知道指针变量是存放地址单元的变量,那么指针变量存放在哪里?指针是变量,通常来说存放在栈区,当指针变量被定义为全局变量或者静态变量的时候,那自然就存放在全局静态存储区。写了个简单的代码,看一下分布就知道指针变量也是有自己的地址的,在图中最下面监视框里,我对变量ptr1取地址操作,可以看到其地址为0x0073fc68,而这个地址中存放的内容是0x00adff98,0x00adff98这个地址是malloc申请到的内存地址。

image.png还有一点需要提一下,也是面试官经常会问到的东西,指针变量的大小是固定的,不管是char指针还是int指针,在x86系统下是4字节大小,在x64系统下是8字节大小。感兴趣的同学可以使用sizeof()函数来自己动手测试一下。


二、指针的作用


每一项伟大的发明,都是来自于“懒人”,这些天才发明者也许是为了自己方便或者舒服,一不小心就会发明一种使千万人收益的东西。


函数传参:我愿意称指针为C语言最伟大的发明之一,当我们需要传递一个很长的字符串,或者需要将一片连续内存传递给一个函数的时候,我们能用什么方法?指针!指针的存在让函数调用过程中开销变小,让调用者不用再烦恼数据该如何传递,只需要传入指向特定位置的地址,就可以达到遍历整片内存的效果,我之前一篇文章讲过形参和实参的区别,其中最后就提到了如果传递的形参是一个指针变量,那么被调函数中对于指针指向的地址修改内容,实参中也会受到影响,这是必然的事情,因为形参和实参指向的是同一片地址空间。


遍历内存:其实我们想要去对一片地址去空间干什么事情的时候,只要能掌握该空间的某个地址,使用指针去游历这片内存,那我们就可以看到任何我们效果看到的东西,甚至可以去修改,去插入我们想要插入的代码段。


三、指针常见的使用场景


此时也许有人会想,说了那么概念(废话),那指针到底怎么用啊?道理我都懂,用起来就迷糊啊!


1. 给一个指针变量赋值

  int a1 = 5;//初始化一个int型变量
  int* ptr1 = NULL;//初始化一个int*指针
  ptr1 = (int*)malloc(10);//给int*指针赋值
  if(ptr1 == NULL)//申请内存失败
  {
    return 0;
  }
  free(ptr1);
  ptr1 = NULL;
  ptr1 = &a1;//再次给int*指针赋值
  printf("%d", *ptr1);

首先来说一个概念,初始化操作和赋值操作,初始化是在创建变量的时候,给变量赋予的一个初始值,而赋值是把当前变量的值修改掉,用一个新的值来代替。一般建议指针变量进行初始化,因为指针虽美,也非常危险(这个后面会提到)。


在上面的代码中,我们首先使用malloc申请了一块内存空间,由ptr1指向这篇内存空间,接着我们又修改了ptr1的指向内容,指向了变量a1所在的地址,说一下&a1读作取地址符a1,意思就是取到a1变量所在的地址空间。最后使用printf打印了指针指向地址的值,用了解引用符号*,*a1就能取到指针指向地址中存放的变量,想打印指针指向的地址,需要写成printf("%d", ptr1);即可


接下来说指针常量和常量指针,这两个也是面试经常会问到的问题:


先来看指针常量:

  const int a1 = 3;
  int* ptr1 = &a1;//编译器报错,ptr1是一个普通指针

上面这个代码不太行,原因就是指针级别不够啊,门不当户不对,那要怎么整啊?

  const int a1 = 3;
  const int* ptr1 = &a1;
  *ptr1 = 5;//这里又不行了

这下好了,捡了芝麻丢了习惯,为啥又不行了啊?因为ptr1指向的是一个常量,学过C++的都应该知道,常量在正常情况下是不允许修改的(虽说强制改也可以),所以不可以给这个指针解引用赋新值。但是ptr1指向的值可以改变,也就是说可以指向其他地址,比如下面的操作是合法的:

  const int a1 = 3;
  const int* ptr1 = &a1;
  const int a2 = 5;
  ptr1 = &a2;

总结就是说指针常量,指向一个常量,被指向的常量值不可以改变,但是指针常量本身可以指向其他的指针常量(有点拗口)。


再来看看常量指针:

  int a1 = 3;
  int* const ptr2 = &a1;

我们把这样的定义,叫做常量指针,这个职责和你指向的值是一个常量,也就是说不可改变,好比我们每个人只有一个身份证,不可以改变。就绑定在一起了。

  int a1 = 3;
  int* const ptr2 = &a1;
  int a2 = 5;
  ptr2 = &a2;//这里会报错

报错的原因就是不可修改指针指向的地址。当然对于指针解引用改变地址空间的值还是可以的:

  int a1 = 3;
  int* const ptr2 = &a1;
  *ptr2 = 5;

还有一种常见的场景那就是指针指向数组的情况,可以使指针指向数组来完成数组遍历。

  int arr[5] = { 1,2,3,4,5 };
  int* ptr1 = NULL;
  ptr1 = arr;//指针指向数组首地址,接下来就可以拿指针玩了
  printf("%d\n", *(ptr1 + 2));//输出第三个数据

image.png


2. 指针在函数方面的应用

2.1指针作为函数参数

指针作为函数参数可以帮程序员带来很大的便利,比如我们要在某个函数内对主调函数中一片内存地址进行操作,此时我们可以将指针作为参数传递。下面来个低级示范:

void function_1(char* Parameter1)//将主调函数中的字符串改为HelloWorld
{
  *Parameter1 = 'H';
  *(Parameter1 + 5) = 'W';
  return;
}
int main()
{
  char* ptr1 = NULL;
  ptr1 = (char*)malloc(15);
  if (ptr1)
  {
    memset(ptr1, 0, 15);
    memcpy(ptr1, "helloworld", 10);
    printf("before  %s\n", ptr1);
    function_1(ptr1);//指针作为参数
    printf("after   %s\n", ptr1);
    free(ptr1);
    ptr1 = NULL;
  }
  return 0;
}

以上代码完全是为了做个例子,其实可以不用调用函数,直接在main函数中就可以实现这个简单操作。来说一下,将源字符串helloworld进行修改,改h和w为大写,只需要传入字符串首地址,然后对地址进行加法操作寻址,进行修改即可。

image.png

2.2 指针作为返回值&&二维指针的优化

以上我们提到的都是一维指针,现在我们要提到二维指针,二维指针一般可以指向二维数组或者用来作为函数参数,最终返回。

最常见的一维指针作为返回值:

char* function_1()
{
  char* ptr1 = NULL;
  ptr1 = (char*)malloc(15);
  if (ptr1)
  {
    memset(ptr1, 0, 15);
    memcpy(ptr1, "helloworld", 10);
  }
  return ptr1;
}
int main()
{
  char* ptr = NULL;
  ptr = function_1();
  printf("%s\n", ptr);
  if(ptr)
  {
    free(ptr);
    ptr = NULL;
  }
  return 0;
}

对于以上代码,我们期待的是它来返回一个字符串“helloworld”,我必须要说的是在这里,它确实也可以实现我们的目的。

image.png


但是!!!但是!!!!就在前几天,我在一个项目中,就是通过这样的方式,返回了一个字符串指针,它根本不是我想要的值??我平时就是这么写的啊,也确实能要到我的结果啊,并且我调试发现,在被调函数返回之前,字符串指针里面的内容也确实是我想要的,但是一旦返回值接到这个指针,里面就是乱码,每次调试指向的的结果还不一样!!我确信我遇到了野指针问题,野指针顾名思义就是比较“狂野”,不知道在指哪儿的指针。我反思了很久,并且用二维指针作为参数来承接返回值任务,最终解决了这个问题。

我个人反思觉得,是因为项目代码过于庞大,堆区地址一直在申请,而对于这种局部变量来说,它的作用域只在当前函数,在fucntion_1函数返回之后,被调函数堆栈随之销毁,而上面的代码能正确返回字符串,是因为此时堆区基本没有被破坏和分割,所以可以得到我们想要的值,但是当项目过大的时候,可能会出现意想不到的bug。


使用二维指针作为参数来优化代码,用来存放返回值指针。


image.pngimage.pngimage.pngimage.png

void function_1(char** Parameter)
{
  *Parameter = (char*)malloc(15);
  if (*Parameter)
  {
    memset(*Parameter, 0, 15);
    memcpy(*Parameter, "helloworld", 10);
  }
  return;
}
int main()
{
  char* ptr = NULL;
  function_1(&ptr);
  printf("%s\n", ptr);
  if(ptr)
  {
    free(ptr);
    ptr = NULL;
  }
  return 0;
}

说一句二维指针,二维指针就是用来存放指针的指针,只不过是多解一次引用而已,没什么神秘的。。传入二维指针参数,然后申请内存,最后在主调函数中去取出内容,预期结果就是输出helloworld:

image.png

2.3 函数指针

函数指针定义的前提:必须知道函数的返回值类型和所传参数类型。

typedef void(*LPFN_FUNCTION_1)(char** Parameter);//函数指针的定义
void function_1(char** Parameter)
{
  *Parameter = (char*)malloc(15);
  if (*Parameter)
  {
    memset(*Parameter, 0, 15);
    memcpy(*Parameter, "helloworld", 10);
  }
  return;
}

我们来定义函数指针,是准备指向function_1的函数指针。定义方法如上代码所示。

在main函数中执行调用:

int main()
{
  LPFN_FUNCTION_1 function_pointer = NULL;
  char* ptr = NULL;
  function_pointer = function_1;
  function_pointer(&ptr);
  printf("%s\n", ptr);
  if(ptr)
  {
    free(ptr);
    ptr = NULL;
  }
  return 0;
}

我们预期的结果肯定是和2.2中输出的结果一样,接下来看下执行效果。

image.png

我们利用监视框看一下函数指针指向的是什么东西:

image.png

确实是指向了function_1函数,所以,指针真的是强大到可以说无所不能!!


3. 指针和引用的区别

如果有过面试经验的人,应该都知道,这也是最常问的一道面试题,“来说一下指针和引用的区别”,此时就有点头大了,哪能说的那么全面呢,就将我知道的几点说给大家听:

3.1 指针和引用的声明与定义:

  int a1 = 3;
  int &v1 = a1;
  int* ptr1 = NULL;
  ptr1 = &a1; 

image.png

调试以上代码可以很清楚的看到变量a1和引用变量v1的地址是指向了同一个地址空间0x00effbf8这个地址,地址中存放了整数3。而指针变量ptr1有自己的内存空间我们之前也提到过这个事情。引用就是变量的一个别名,和变量共享同一个内存单元,而指针变量有自己的地址空间。声明引用变量是必须初始化:

  int &v1;//错误

类似这样的声明是错误的,因为这个变量必须有个内存单元来让它命名。


3.2 指针和引用的使用区别

在使用指针的过程中可以让指针指向其他地址,但是引用变量一旦初始化之后,就不允许再次改变。这个特性也导致指针比较危险(有可能会指向非法内存区域),引用比较安全,但是指针相对灵活,所以各有利弊。

  int a1 = 3;
  int &v1 = a1;
  int* ptr1 = NULL;
  ptr1 = &a1; 
  int b = 6;
  ptr1 = &b;

以上操作是合法的,ptr1指向了b所在的地址。

对于变量的操作方法也是不一样的,如果我们要改变ptr1指针指向地址里面存放的值,那就得解引用才能操作,而引用变量则相当于变量的别名,可以直接对其修改:

  int a1 = 3;
  int &v1 = a1;
  int* ptr1 = NULL;
  ptr1 = &a1; 
  *ptr1 = 9;  //修改a1的值 此时a1 = 9 v1 = 9 *ptr1 = 9
  v1 = 10;    //修改a1的值  此时a1 = 10 v1 = 10 *ptr1 = 10

以上ptr1和v1的修改操作,是对同一片地址空间的操作。

3.3 指针和引用作为函数参数

指针和引用都可以作为函数参数进行传参,那么这两个在传参的过程中有没有什么不同呢?

void function_1(int& Parameter1,char* Parameter2)
{
  Parameter1 = Parameter1 + 1;
  Parameter2 = Parameter2 + 1;
  return;
}
void function_2(int Parameter3, char* Parameter4)
{
  Parameter3 = Parameter3 + 1;
  Parameter4 = Parameter4 + 1;
  return;
}
int main()
{
  int a1 = 3;
  int &v1 = a1;
  char* ptr1 = NULL;
  ptr1 = (char*)malloc(15);
  if (ptr1)
  {
    memset(ptr1, 0, 15);
    memcpy(ptr1, "hello", 5);
    function_1(v1, ptr1);//第一参数是引用变量,第二参数是指针
    function_2(a1, ptr1);//第一参数是普通int变量,第二参数是指针
    free(ptr1);
    ptr1 = NULL;
  }
  return 0;
}

我们定义了function_1和function_2两个函数,两个函数的第一参数不同,第一个是整型引用变量,第二个是整型变量。看一下同样的加1操作,在执行时有何不同。看下汇编代码:

image.png

image.png

在汇编代码的红色框中可以看出,引用变量需要两次的dword ptr[]指令才执行add指令,所以引用变量作为形参传递时候,改变形参的值也将会导致实参的值发生改变。而function_2中的形参,只需要一次 dword ptr[]指令便可以add操作,所以这个形参的改变不会影响实参的值。


根据汇编代码可知,对于指针变量的++操作同理也只是影响形参的值,想要影响实参的值,可以将指针进行解引用*ptr1修改其中的字符串内容,则会达到和引用变量相同的效果,修改实参的值。


总结:引用作为函数的一个参数传递,修改该参数,可以达到修改实参的效果,指针作为参数,进行解引用操作,也可达到修改实参的效果


3.4 对指针和引用执行++操作

我们都知道连加操作的作用,比如int型变量v1,v1++,相当于v1=v1+1。那对于指针和引用是怎样的情况呢?


image.png

执行++操作前,ptr1指向的地址为0x006ff9a0,v1地址与ptr1指向的相同,然后执行++操作:

image.png

可以看到ptr1指向的地址发生了4字节偏移,而v1++操作是对v1地址中存放的整形变量3进行了+1操作。


总结:指针++操作会使指针指向的地址发生指针对应类型的字节偏移,例如int 型指针进行++,会让指针偏移4字节,如果char*指针进行++,会让指针偏移1字节。而引用变量++是对地址中存放的变量执行++操作。


4. this指针

this指针这个东西在C语言中是没有的,其实这个东西应该在C++类的有关知识点再讲的,此处就简单的提一下。如果有想学C++或者对C++感兴趣的,可以看一下这里,如果只是学C语言,那也可以不看,但是既然能有耐心把文章看到这里,不妨接着往下看,把它看完吧!

class TestClass
{
public:
  TestClass();
  ~TestClass();
  int function_1(int a);
  int function_2();
private:
  int a;
public:
  int d;
};
TestClass::TestClass()
{
  this->a = 1;
  this->d = 4;
}
TestClass::~TestClass()
{
}
int TestClass::function_1(int a)
{
  //this指针可以用来区分外部参数和类内部成员变量
  printf("this->a: %d\n", this->a);//这样才可以保证打印的类内成员a
  printf("a: %d\n", a);//参数a
  this->function_2();//this对类内函数的操作
  printf("d: %d\n", d);
  return 0;
}
int TestClass::function_2()
{
  d++;
  return 0;
}

这是一个类的定义,其中内部成员变量有私有成员a,公有成员d。在function_1中传入的形参也是a,如果不是用this来区分,那就会按照作用域就近原则,对a的操作都默认是对当前形参a的操作而不是成员变量a。

int main()
{
  TestClass c1;
  c1.function_1(3);
  return 0;
}

我们来看一下代码输出效果:

image.png

总结:this指针是当前类的指针,对类内除了静态成员和静态函数外的其他成员以及函数都可以进行操作。


今天脑子里面一直有一句诗,也不知为什么,所以此情此景,我按照惯例,又得吟诗一句:“曲径通幽处,禅房花木深。”

参考资料:

《C++ Primer》第五版


目录
相关文章
|
16天前
|
安全 C语言
【C语言】如何规避野指针
【C语言】如何规避野指针
20 0
|
17天前
|
网络协议 程序员 编译器
C语言:编程世界的基础与魅力
C语言:编程世界的基础与魅力
|
17天前
|
存储 算法 程序员
C语言:基础与应用的双重魅力
C语言:基础与应用的双重魅力
|
17天前
|
C语言
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
|
6天前
|
C语言
c语言指针总结
c语言指针总结
12 1
|
12天前
|
存储 程序员 C语言
【C 言专栏】C 语言指针的深度解析
【4月更文挑战第30天】C 语言中的指针是程序设计的关键,它如同一把钥匙,提供直接内存操作的途径。指针是存储其他变量地址的变量,通过声明如`int *ptr`来使用。它们在动态内存分配、函数参数传递及数组操作中发挥重要作用。然而,误用指针可能导致错误,如空指针引用和内存泄漏。理解指针的运算、与数组和函数的关系,以及在结构体中的应用,是成为熟练 C 语言程序员的必经之路。虽然挑战重重,但掌握指针将增强编程效率和灵活性。不断实践和学习,我们将驾驭指针,探索更广阔的编程世界。
|
13天前
|
算法 搜索推荐 程序员
C语言中的函数指针和回调函数
C语言中的函数指针和回调函数
10 2
|
13天前
|
关系型数据库 MySQL 编译器
探索C语言的魅力:从基础到实践
探索C语言的魅力:从基础到实践
23 3
|
13天前
|
存储 自然语言处理 Unix
C语言:探索其魅力与深度
C语言:探索其魅力与深度
22 5
|
16天前
|
存储 编译器 C语言
【C语言】初步解决指针疑惑
【C语言】初步解决指针疑惑
7 0