C语言指针总结

C语言指针总结0 前言 指针是C语言最重要也是最难理解的部分,它在我们平时的工作中无处不在。有人说学会了指针,C语言也就学会一半。

0 前言

指针是C语言最重要也是最难理解的部分,它在我们平时的工作中无处不在。有人说学会了指针,C语言也就学会一半。为什么说指针难。因为指针与数组相结合就涉及数组指针与指针数组。指针与结构体结合就涉及结构体指针。指针与字符结合涉及字符指针。指针与const结合涉及常量指针与指针常量。指针与函数结合涉及函数指针与指针函数,同时也会涉及回调函数。指针和指针结合涉及到二维指针。作者曾经因为上面的这些问题,困扰了许久。因而在网上找了许多的博客来解答疑惑。这篇文章,我试图将上面的知识点以例子的方式呈现给大家,我相信通过阅读本文,大家会对指针有更深一步的了解。文中涉及的例子均来源于网上。

1 指针的定义

我们知道,普通的变量存储的是一个值。而指针变量,它存储的也是一个值,只是这是一个特殊的值:它的值是另一个变量的地址

指针的定义形式如下:

datatype *name;
或 
datatype *name = value;

其意思就是name是一个指针,它指向的是一个类型为dataype的地址。 指针存储的是一个地址,如果需要获取这个地址对应的内容,可以通过解引用符*获取:

int a = 12;
int *pa = &a;
printf("*pa:%u.", *pa); // 输出是12;
*pa = 14; // 此时a的值为14了

这里需要注意的一点,也是我以前经常迷惑的一点: 定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义时同时赋给一个字符串常量进行初始化。 比如:

int *a;
...
*a = 12;

上面这个代码段说明了一个极为常见的错误:我们声明了这个变量,但从未对它进行初始化,所以没法预测12这个值将存储于什么地方。如果变量是静态的,它会被初始化为0,;如果变量是自动地,它根本不会被初始化。无论哪种情况,声明一个指向整型的指针都不会”创建”用于存储整型值的内存空间。 但是, 下面的定义创建了一个字符串常量(为其分配了内存):

char *p = "breadfruit";

初始化指针时所创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。

除了上述的定义是对的外,其他的定义都是错误的:

float *pip = 3.14; // 错误!无法通过编译

2 指针的运算

  • 指针 +(-) 整数 指针存储的是一个地址,这个地址本质上是一个整数,所以可以加上或减去一个整数。但是它不是普通的加法或减法,指针加上或减去一个整数结果是另一个指针。但是,运算后的指针指向哪里呢?当一个指针和一个整数执行算术运算时,整数在执行加法(减法)运算前会根据合适的大小进行调整。这个”合适的大小”就是指针所指向类型的大小,”调整”就是把整数值和”合适的大小”相乘。
#include <stdio.h>
int main()
{
    int a = 10;
    int *pa = &a;
    
    double b = 99.9;
    double *pb = &b;
    char c = '@';
    char *pc = &c;
    printf("sizeof(int)= %u, sizeof(double)=%u, sizeof(char)=%u\n",
        sizeof(int), sizeof(double), sizeof(char));

    //最初的值
    printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);

    //加法运算
    pa++; pb++; pc++;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);
    //减法运算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);
    return 0;
}

运算结果:

sizeof(int)= 4, sizeof(double)=8, sizeof(char)=1
&a=000000000061FE04, &b=000000000061FDF8, &c=000000000061FDF7
pa=000000000061FE04, pb=000000000061FDF8, pc=000000000061FDF7
pa=000000000061FE08, pb=000000000061FE00, pc=000000000061FDF8
pa=000000000061FE00, pb=000000000061FDF0, pc=000000000061FDF6

由上面的结果可以看到,当对指针pa,pb,pc进行加1时,实际地址增加的是对应类型的大小。减法也一样。

  • 指针 – 指针 只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。两个指针相减的结果是两个指针之间的元素个数。比如,如果p1指向array[i]而p2指向array[j],那么p2-p1的值就是j-i的值。如果两个指针所指向的不是同一个数组中的元素,那么它们之间相减的结果是未定义的,也是毫无意义的。

3 指针与数组

3.1 数组指针(指向数组的指针)

数组指针,它是一个指针,指向的是一个数组。即它存的是一个数组变量的地址。所以这个指针每加一步的步长就是数组的长度。由于它每跨一步都是整个数组,所以又称行数组。

#include <stdio.h>
int main()
{
    int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
    int (*pa)[4];
    pa = a;
    printf("a:%p, &a:%p, &a[0][0]:%p\n", a, &a, &a[0][0]);
    printf("pa:%p, (*pa)[0]:%u\n", pa, (*pa)[0]);
    pa++;
    printf("&a[1]:%p, &a[1][0]:%p\n", &a[1], &a[1][0]);
    printf("pa:%p, (*pa)[0]:%u\n", pa, (*pa)[0]);
    return 0;
}

运行结果:

a:000000000061FDE0, &a:000000000061FDE0, &a[0][0]:000000000061FDE0
pa:000000000061FDE0, (*pa)[0]:1
&a[1]:000000000061FDF0, &a[1][0]:000000000061FDF0
pa:000000000061FDF0, (*pa)[0]:5

首先,pa是一个数组指针,它首先存的是数组a的首元素的地址,由于数组名也是数组的首地址,所以a, &a, &a[0][0]的地址相同。pa中存的也是这个地址。然后对pa进行解引用,*pa之后得到这个数组,然后(*pa)[i]就是获得这个数组下标为i的元素。

3.2 指针数组

指针数组,它本质上是一个数组,只不过整个数组存的类型是一个指针而已。

#include<stdio.h> 

int main(void)
{
    char *p1 = "Himanshu";
    char *p2 = "Arora";
    char *p3 = "India"; 

    char *arr[3]; 

    arr[0] = p1;
    arr[1] = p2;
    arr[2] = p3; 

   printf("\n p1 = [%s] \n",p1); printf("\n p2 = [%s] \n",p2); printf("\n p3 = [%s] \n",p3); printf("\n arr[0] = [%s] \n",arr[0]); printf("\n arr[1] = [%s] \n",arr[1]); printf("\n arr[2] = [%s] \n",arr[2]); return 0; } 

运行结果:

p1 = [Himanshu]

p2 = [Arora]

p3 = [India]

arr[0] = [Himanshu]

arr[1] = [Arora]

arr[2] = [India]

参考文献:www.thegeekstuff.com/2012/01/adv…

4 指针与字符

在C语言中,表示字符串一般有两种形式,一种是数组的形式,一种是字符指针的形式。 数组形式:

char arr[] = "hello,world";

字符指针形式:

char *str = "hello,world";

虽然上面两种形式都能表示字符串,但是它们还是有些区别的:

  1. 存储方式 字符数组由若干元素组成,每个元素存放一个字符,而字符指针变量只存放字符串的首地址,不是整个字符串。

  2. 存储位置。 数组是在内存中开辟了一段空间存放字符串, 是存在栈区。而字符指针是在字面值常量区开辟了一段空间存放字符串,将字符串的首地址付给指针变量str。

  3. 赋值方式。 对于数组,下面的赋值方式是错误的:

char str10];
str"hello"; // 错误!

而对字符指针变量,可以采用下面方法赋值:

char *a;
a = "hello";
  1. 可否被修改。 指针变量指向的字符串内容不能被修改,但指针变量的值(即存放的地址或者指向)是可以被修改的。

5 指针与结构体

一个指针,它指向的可以是一个结构体类型,这称为结构体指针。而一个结构体,它的成员中也可以有指针成员。

struct{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

上面的代码中,定义了一个结构体变量stu1。这个变量中有一个指针变量name。还定义了一个结构体指针pstu。 我们想要通过结构体指针访问结构体成员一般有以下两种方式:

(*pstu).name;
pstu->name;

6 指针与const:常量指针与指针常量

参考文献: www.thegeekstuff.com/2012/06/c-c… eli.thegreenplace.net/2003/07/23/…

初学者常常对这两个概念搞错。首先,我认为需要理解这里说的常量是什么意思。常量就是只可读不可修改 的。那常量指针和指针常量到底哪个是只可读不可修改的呢?是指针还是指针指向的内容? 这里有一个方法,能让你迅速明白哪个是不可修改的。就是在声明时,以星号(*)为界,分成两部分,星号左边的和星号右边的。const在哪边,那个就是只可读不可修改的。 以下面这个代码为例,我们来分析一下:以星号(*)为界,星号左边是char,没有const关键词,所以它指向的内容不是常量。然后,我们看星号的右边是const ptr,所以我们可以说ptr是一个常量。所以,这行代码声明了一个是常量的指针但是指向的内容不是常量。即这个是一个指针常量。

char* const ptr = "just a string";

类似的,我们也可以分析下面的代码:

// Neither the data nor the pointer are const
//
char* ptr = "just a string";

// Constant data, non-constant pointer
//
const char* ptr = "just a string";

// Constant pointer, non-constant data
//
char* const ptr = "just a string";

// Constant pointer, constant data
//
const char* const ptr = "just a string";

6.1 指针常量(Constant Pointers)

指针常量(Constant Pointers): 它的本质是一个常量,只不过这个常量是指针。由于指针是只可读不可修改的,所以这个指针不能指向别的地址了,但是该地址里的内容还是可以改变的。 指针常量的声明格式如下:

<type of pointer> * const <name of pointer>
例如: int * const ptr;

我们来看下面的这段程序:

#include<stdio.h>
int main(void) {
    int var1 = 0, var2 = 0;
    int *const ptr = &var1;
    ptr = &var2;
    printf("%d\n", *ptr);

    return 0;
}

上面这段程序中:

  • 我们首先定义了两个变量var1,var2;
  • 然后,定义了一个指针常量ptr,并且指向了var1
  • 接着,试图让ptr指向var2
  • 最后,打印出指针ptr指向的地址的内容

让我们来运行一下这个程序:

main.c: In function 'main':
main.c:6:9: error: assignment of read-only variable 'ptr'
     ptr = &var2;
         ^

我们看到这个程序编译报错了:试图对只读(read-only)变量ptr进行赋值。所以,一旦我们定义了指针常量,那这个指针就不能指向其他变量了。 但是我们还是可以修改指向的地址里的内容的:

#include<stdio.h>
int main(void) {
    int var1 = 0;
    int *const ptr = &var1;
    *ptr = 10; // OK
    printf("%d\n", *ptr); // 10
    return 0;
}

6.2 常量指针(Pointer to Constants)

常量指针(Pointer to Constants):它的本质是一个指针,只不过它指向的值是常量(只可读,不可修改)。由于指向的是一个只可读不修改的值,所以指针不能通过它存储的地址间接修改这个地址的值,但是这个指针可以指向别的变量。

常量指针的声明格式如下:

const <type of pointer>* <name of pointer>
例如: const int* ptr;

还是有一段程序:

#include<stdio.h>

int main(void) {
    int var1 = 0;
    const int* ptr = &var1;
    *ptr = 1;
    printf("%d\n", *ptr);

    return 0;
}

我们还是来分析一下这个程序:

  • 我们定义了一个变量 var1,并且初始化为0
  • 然后我们定义了一个指针常量ptr,并且将它指向了var1
  • 接着,试图通过指针ptr来改变var1的值
  • 最后,打印出ptr指向的地址的内容。

我们进行编译:

main.c: In function 'main':
main.c:7:10: error: assignment of read-only location '*ptr'
     *ptr = 1;
          ^

编译报错也很明显: *ptr是一个只读的。所以不能通过ptr来修改var1的值。 但是,我们可以将ptr指向其他的变量:

#include<stdio.h>

int main(void) {
    int var1 = 0;
    const int* ptr = &var1;
    printf("%d\n", *ptr); // 0
    int var2 = 20;
    ptr = &var2; // OK
    printf("%d\n", *ptr); // 20
    return 0;
}

6.3 指向常量的常量指针

理解了上面两种类型的话,理解这个就很容易了。指向常量的常量指针是指这个指针既不能指向其他的地址也不能通过地址修改内容。 它的声明格式如下:

const <type of pointer>* const <name of pointer>
例如: const int* const ptr;

同样,下面一段程序,我想你一定知道哪里编译错误了。

#include<stdio.h>

int main(void)
{
    int var1 = 0,var2 = 0;
    const int* const ptr = &var1;
    *ptr = 1;
    ptr = &var2;
    printf("%d\n", *ptr);

    return 0;
}

编译结果:

main.c: In function 'main':
main.c:7:10: error: assignment of read-only location '*ptr'
     *ptr = 1;
          ^
main.c:8:9: error: assignment of read-only variable 'ptr'
     ptr = &var2;
         ^

7 指针与函数

7.1 函数指针

指针与函数相结合有两种情况:指针函数、函数指针。 指针函数,它的本质是一个函数,它的返回值是一个指针。

int * func(int x, int y);

参考:blog.csdn.net/qq_33757398…

函数名本身就是一个指针(地址),这个地址就是函数的入口地址。

#include <stdio.h>
int sum(int a, int b) {
	return a + b;
}
 
int main() {
	printf("%p\n", sum);
	return 0;
}

输出: 0000000000401550

而函数指针,它的本质是一个指针。只不过它存的地址恰好是一个函数的地址罢了。 函数指针变量定义的格式一般是:

返回值 (*变量名)(参数列表)

比如:

#include <stdio.h>
int sum(int a, int b)
{
    return a + b;
}
 
int main()
{
    printf("%p\n", sum);
    int (*psum)(int, int);  // 函数指针变量,参数名可以省略
    psum = sum;
    printf("%p\n", psum);
    return 0;
}

输出:

0000000000401550
0000000000401550

可以发现,两者地址相等。

函数指针类型的定义:

typedef  返回值 (* 类型名)(参数列表);

比如:

typedef int(*PSUM)(int, int);
PSUM pSum2 = sum;
PSUM pSum3 = sum;

这样的好处就是,首先通过typedef定义一个函数指针类型PSUM,定义完后,PSUM就相当于一种新的类型,可以用此类型去定义其他函数指针变量,就不用每次都使用int(*pSum)(int, int);来定义一个函数指针变量。

#include <stdio.h>
int sum(int a, int b) {
    return a + b;
}
int func2(int a, int b) {
    return a - b;
}
typedef int (*PFUNC) (int, int);
int main() {
    int (*psum)(int, int);
    psum = sum;
    printf("psum(4, 5):%d\n", psum(4, 5));
    PFUNC p2 = func2;
    printf("p2(5, 2):%d\n", p2(5, 2));
    p2 = sum;
    printf("p2(5, 2):%d\n", p2(5, 2));
    return 0;
}

输出:

psum(4, 5):9
p2(5, 2):3
p2(5, 2):7

7.2 回调函数

参考:zhuanlan.zhihu.com/p/214157226

说到函数指针,那还有一个概念不得不提——回调函数。因为在实际的项目代码中实在是太常见了。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

那为什么要使用回调函数呢?或者说使用回调函数有什么好处呢?回调函数允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似的事情时,可以灵活的使用不同的方法。

怎么使用回调函数:

#include <stdio.h>
int Callback_1(int a) ///< 回调函数1 {
    printf("Hello, this is Callback_1: a = %d \n", a);
    return 0;
}

int Callback_2(int b) ///< 回调函数2 {
    printf("Hello, this is Callback_2: b = %d \n", b);
    return 0;
}

int Callback_3(int c) ///< 回调函数3 {
    printf("Hello, this is Callback_3: c = %d \n", c);
    return 0;
}

int Handle(int x, int (*Callback)(int)) // 注意这里用到的函数指针定义 {
    Callback(x);
}

int main() {
    Handle(4, Callback_1);
    Handle(5, Callback_2);
    Handle(6, Callback_3);
    return 0;
}

如上述代码:可以看到,Handle()函数里面的参数是一个指针,在main()函数里调用Handle()函数的时候,给它传入了函数Callback_1()/Callback_2()/Callback_3()的函数名,这时候的函数名就是对应函数的指针,也就是说,回调函数其实就是函数指针的一种用法。

8 二维指针

二维指针,或者二级指针。就是指向指针的指针。比如:

#include<stdio.h>
int main()
{
    int a = 10;
    int *pa = &a;
    int **ppa = &pa;
    printf("%p, %p, %p, %p, %p", a, pa, *pa, ppa, *ppa);
    return 0;
}

输出如下:

000000000000000A, 000000000061FE14, 000000000000000A, 000000000061FE08, 000000000061FE14

从输出结果也可以看到,pa存的内容*pa= 000000000000000A,刚好与a的地址相同。而ppa存的内容*ppa= 000000000061FE14也刚好等于pa的地址。它们之间的内存关系可以用如下的图表示:

二维指针的内存关系.png

说到二维指针,那二维数组就不得不提了。我之前写过一篇关于二维数组与二维指针的,大家可以参考这篇文章

8.1 命令行参数

处理命令行参数是指向指针的指针的一个用武之地。

一般main函数具有两个形参。第一个通常称为argc,它表示命令行参数的数目。第2个通常称为argv,它指向一组参数值。由于参数的数目并没有内在的限制,所以argv指向这组参数值(从本质上来说是一个数组)的第一个元素。这些元素的每个都是指向一个参数文本的指针。如果程序需要访问命令行参数,main函数在声明时就要加上这些参数。

int main(int argc, char **argv)

举例:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("argc: %d\n", argc);

    // 打印参数,直到遇到NULL指针。程序名被跳过
    while (*++argv != NULL) {
        printf("%s\n", *argv);
    }
    return 0;
}

在windows上执行: \test2.exe hello world 输出:

argc: 3
hello
world

注意,如果命令行中传递的一个参数包括空格,就需要用 “”将参数括起来,比如: .\test2.exe “hello word” 则上面的代码将输出:

argc: 2
hello word

9 结束语

本文关于指针的讲解就结束了。我相信你一定对指针有更深入的了解。大家如果觉得本文不错的话,欢迎点赞、收藏与转载。转载请注明出处

今天的文章C语言指针总结分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14469.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注