初级指针

本篇主要介绍:指针和变量的关系、指针类型、指针的运算符、空指针和野指针、指针和数组指针和字符串、const 和指针、以及gdb 调试段错误

基础概念

指针是一种特殊的变量。存放地址的变量就是指针。

int num = 1; 会申请4个字节的内存来存放数字1,每次访问 num 就是访问这4个字节。

访问内存中的这4个字节,不仅可以通过名称(例如 num),还可以通过地址

Tip& 不仅是位运算符,还是取地址操作符。例如 int* ptr = #,就是取变量 num 的地址并将其保存到指针变量 ptr 中

请看示例:

#include int main() {    int num = 10;    // num 的地址:0x7fff4dbf01d8    printf("num 的地址:%p\n", &num);    // num 的地址加1 :0x7fff4dbf01dc。    printf("num 的地址加1 :%p\n", &num + 1);    // j 存放连续内存的第一个字节地址    int *j = #    // 10。通过地址访问    printf("%d", *j);    return 0;}

&num&num + 1 相差4个字节,说明 &num 表示整数。

普通变量存放值,而指针用于存放地址。

通过 int *j = &num 将变量num的首地址给到指针 j(j的类型是 int *),最后通过地址(*j) 访问整数1。

int *j 是一个int类型的指针,还有 char、float等指针类型。指针类型必须匹配,比如将 j 的指针类型换成 char,则会警告。就像这样:

- int *j = #+ char *j = #

运行:

/workspace/CProject-test/main.c:12:11: warning: incompatible pointer types initializing 'char *' with an expression of type 'int *' [-Wincompatible-pointer-types]    char *j = #          ^   ~~~~1 warning generated.num 的地址:0x7ffddcfe5328num 的地址加1 :0x7ffddcfe532c10

Tip: 指针 j 也有地址,也就是指针的指针。现在不研究

练习

题目:请问输出什么?

#include int main() {    int num = 10;    int *p = #    printf("用指针访问数据 num :%d\n", *p);    *p = 11;    printf("用过指针修改 num 数据:%d\n", num);    return 0;}

提示:数据可以通过变量访问,也能使用地址(指针)访问。就像通知同学去嵌入式实验室上课,或者是 303 上课。其中*p = 11; 等价于 num = 11;

输出:

用指针访问数据 num :10用过指针修改 num 数据:11

星号的作用

指针 * 有两个主要作用(根据* 前面有无类型做区分):

  • 指针类型声明
  • 取值(又称解引用操作符)。例如,*ptr 表示获取指针变量 ptr 所指向内存地址上的值。

请看示例:

#include int main() {    int num = 10;    // 指针类型声明    int *p = #    // 取值    printf("%d\n", *p); // 10    // 取值    *p = 11;    printf("%d\n", num); // 11    return 0;}

指针类型所占字节

在32位系统上,指针通常占用4个字节;而在64位系统上,指针通常占用8个字节。请看示例:

#include int main() {    printf("char类型指针所占字节数为:%zu\n", sizeof(char*));    printf("short类型指针所占字节数为:%zu\n", sizeof(short*));    printf("int类型指针所占字节数为:%zu\n", sizeof(int*));    printf("long类型指针所占字节数为:%zu\n", sizeof(long*));    printf("float类型指针所占字节数为:%zu\n", sizeof(float*));    printf("double类型指针所占字节数为:%zu\n", sizeof(double*));    printf("long long类型指针所占字节数为:%zu\n", sizeof(long long*));    return 0;}

输出:

char类型指针所占字节数为:8short类型指针所占字节数为:8int类型指针所占字节数为:8long类型指针所占字节数为:8float类型指针所占字节数为:8double类型指针所占字节数为:8long long类型指针所占字节数为:8

练习

题目:请问整数类型的指针和字符类型的指针加1分别是几个字节?

#include int main() {    int num = 10;    printf("num 的地址:%p\n", &num);    printf("num 的地址加1 :%p\n", &num + 1);    char ch = 'a';    printf("ch 的地址:%p\n", &ch);    printf("ch 的地址加1 :%p\n", &ch + 1);    return 0;}

输出:

num 的地址:0x7fffe8244288num 的地址加1 :0x7fffe824428cch 的地址:0x7fffe8244287ch 的地址加1 :0x7fffe8244288

答案int * 加1是4个字节;char * 加1是1个字节。&num 和 &ch 分别代表该变量的全部字节。

指针交换数据

比如这段代码是不能实现 a、b 两数交换。请看示例:

#include void swap(x, y){    int tmp = x;    x = y;    y = tmp;}int main() {    int a = 1;    int b = 2;    swap(a, b);    printf("a:%d\n", a);    printf("b:%d\n", b);    return 0;}
a:1b:2

分析:调用 swap(a, b) 这里是一个值传递,找到函数入口地址,对参数 x、y 申请空间和赋值,通过 tmp 变量完成了 x和y的交换,最后回收局部变量 x、y和tmp,释放空间。而 a,b数据没有变化。

可以通过指针来实现两数的交换。请看示例:

#include void swap(int* x, int* y){    int tmp = *x;    *x = *y;    *y = tmp;}int main() {    int a = 1;    int b = 2;    swap(&a, &b);    printf("a:%d\n", a);    printf("b:%d\n", b);    return 0;}
a:2b:1

分析:通过 swap(&a, &b) 将 a b 的地址传给 x 和 y,通过 x 和 y 指针对 a 和 b 进行交换,虽然最后会销毁swap中的局部变量,但 a 和 b的值已经完成了交换。

指针的运算符

指针和变量的关系

练习1

题目:输出什么?

#include int main() {    int a = 10, *pa = &a, *pb;    printf("%d\n", *pa);    pb = pa;    printf("%d\n", *pb);    return 0;}

输出:10 10

分析:

int a = 10, // pa 指向变量 a*pa = &a, // 定义一个整数型的指针 pb*pb;printf("%d\n", *pa);// pb 也指向变量 apb = pa;printf("%d\n", *pb);return 0;

练习2

题目:输出什么?

#include int main() {    int x = 3, y = 0, *px = &x;    y = *px + 5;    printf("%d\n", y);    y= ++*px;    printf("%d\n", y);    printf("%p\n", px);    y = *px++;     printf("%p\n", px);    printf("%d\n", y);    return 0;}

输出:

840x7ffc48b9be380x7ffc48b9be3c4

分析:

  • y= ++*px; 等效 ++(*px)。如果是 ++* 是不对的
类似 y = ++i,等于先执行 ++,在执行 y = i,这里先对 (*px) 执行 ++,在返回  *px 的值
  • y = *px++;
先执行 y = *px,然后是 px++。px是整数类型的地址,加1就是加4个字节。

练习3

题目:输出什么?

#include int main() {    int x = 3, y = 0, *px = &x;    printf("%p\n", px);    y = (*px)++;     printf("%p\n", px);    printf("%d\n", x);    return 0;}

输出:

0x7ffef1dc4d580x7ffef1dc4d584

分析:*px++ 表示指针加1,(*px)++ 表示值加1。

指针初始化

指针初始化有两种方法:已经存在的空间和自己申请空间。

已经存在的空间,例如:

#include #include int main() {    int num;    int* p = #    *p = 10;    char *str = "abc";    printf("%s\n", str); // abc。把字符串的地址赋值给指针变量    return 0;}

自己申请空间可以使用 malloc 函数。申请的是 void 类型指针,也称为通用类型指针。请看示例:

#include // malloc 需要引入 #include int main() {    // 申请16个字节    int* q = malloc(sizeof(int) * 4); // 在堆里申请了16个字节    // int* q = (int *)malloc(sizeof(int) * 4); // 推荐        *q = 10;    // 释放申请的16个字节    free(q);    return 0;}

申请空间,使用完需要使用 free() 释放。

Tip:根据 C99 标准以及更高版本的标准,显式的类型转换是建议的做法,以确保类型的安全性和可读性。

空指针和野指针

下面这段代码 p 就是一个野指针,运行报错:段错误 (核心已转储)

#include int main() {    int* p;    *p = 1;    return 0;}

这里声明一个指针 p,里面是一个随机数,例如 0x7ffe71df3f40,接着往指向的内存放1,由于这块内存不知道是否存在,即使存在也不能访问,于是报段错误

直接手写一个地址也不可以。就像这样:

#include int main() {        // warning: incompatible integer to pointer conversion initializing 'int *' with an expression of type 'long' [-Wint-conversion]    // 这个警告是因为你正在将一个 long 类型的表达式赋值给一个 int* 类型的指针变量,导致类型不匹配。    // int* p = 0x7ffe71df3f40;    int* p = (int *)0x7ffe71df3f40;    *p = 100;    return 0;}
// 分段错误 (核心已转储)"Segmentation fault (core dumped)

空指针也不能使用:

int* p = NULL;*p = 100;// 输出:`Segmentation fault (core dumped)`

但空指针会让你可控。就像这样:

int* p = NULL;if (p != NULL) {    printf("p is not NULL\n");}else{    printf("p is NULL\n");}// 输出:p is NULL

指针和数组

指针当数组用

遍历一个数组,可以这样:

#include int main() {    int arr[] = {1, 2, 3, 4, 5};    int length = sizeof(arr) / sizeof(arr[0]);  // 计算数组的长度    // 1 2 3 4 5     for (int i = 0; i < length; ++i) {        printf("%d ", arr[i]);    }    return 0;}

使用指针遍历数组有两种方式(效果相同)。请看示例:

#include int main() {    int arr[] = {1, 2, 3, 4, 5};    int length = sizeof(arr) / sizeof(arr[0]);  // 计算数组的长度    // 指针遍历方式1    /*    int* pArr = arr;    for (int i = 0; i < length; ++i) {        printf("%d ", *(pArr + i));    }    */    // 指针遍历方式2    int* pArr = arr;    for (int i = 0; i < length; ++i) {        printf("%d ", pArr[i]);    }    return 0;}

Tip:在数组一文中我们知道数组名表示首元素地址,这里*(pArr + i)会依次遍历数组或许是因为指针是int类型吧!

总结pArr[i] 等于 *(pArr + i)。在这里[]不再是取某个索引,而是表示取值。

指针和字符数组

题目:分析 char a[] = "Hello";char *b = "World";

  • 都可以用for遍历元素。例如:
#include int main() {    char a[] = "Hello";    char *b = "World";    // Iterating over 'a'    printf("Characters in 'a':\n");    for (int i = 0; a[i] != '\0'; i++) {        printf("%c\n", a[i]);    }    // Iterating over 'b'    printf("\nCharacters in 'b':\n");    for (int i = 0; b[i] != '\0'; i++) {        printf("%c\n", b[i]);    }    return 0;}

输出:

开始运行...Characters in 'a':HelloCharacters in 'b':World运行结束。
  • 为什么指针也可以通过索引访问特定字符?
    比如 char *b = "World";,可以将字符串视为字符数组,使用指针来指向该数组的首地址,指针可以通过偏移来访问特定位置的元素,包括字符串中的字符。

练习

题目:下面代码中 p1[0]p2[0]p3[0]的值分别是多少?

// 申请4*4个字节,每个字节地址假如是:0x100(存放1) 0x104(存放2) 0x108 0x10cint a[] = {1,2,3,4};int *p1 = (int*)(&a + 1);int *p2 = (int*)((int)a + 1);int *p3 = (int*)(a + 1);

分析:

  • (int*)(&a + 1) – &a 表示整个数组,加1则到下一个数组,然后将数组指针强转成整数指针,指向第5个元素,其实已经越界了。
  • (int*)((int)a + 1) – a 表示数组首元素地址,(int)a 将地址转为整数,以前是加1个元素,现在就是加1,然后又将整数转为整数指针,乱了(就好比访问 0x101 0x102 0x103 0x104
  • (int*)(a + 1) – a 表示数组首元素地址,加1则是第二个元素地址 0x104,不强转也可以。

结论:只有p3[0](等价于 *(p3 + 0))是一个正常的元素,也就是2.

指针和字符串

题目:用数组和指针定义字符串有什么区别?

#include int main() {    char str[] = "HelloWorld";    // HelloWorld    printf("%s\n", str);    char* s = "HelloWorld";    // HelloWorld    printf("%s\n", s);    return 0;}

Tip: 字符串的输出都是首地址,比如这里的 str 是数组的首地址,s 指针指向的也是首地址。

分析:
char str[] = "HelloWorld"; 在栈中定义一个数组,用11个字节存储HelloWorld(还有一个 \0)。请看示例:

#include int main() {    char str[] = "HelloWorld";    str[0]++;     // IelloWorld    printf("%s\n", str);     // error: cannot increment value of type 'char[11]'    str++;    // printf("%s\n", str);    return 0;}

数组名(str++)不可以修改,str 就是数组首元素地址,已经固定了,可认为它是常量。但数组内容可以修改。

char* s = "helloWorld";helloWorld 放在只读数据区,s 是局部变量,放在栈中,占8个字节。请看示例:

#include int main() {    char* s = "helloWorld";        s++;    // elloWorld    printf("%s\n", s);    // 报错:Segmentation fault (core dumped)    s[0]++;    return 0;}

指针可以加加,但指针指向的内容不能修改。

str 只是个名字,不占空间,如果一定要说占多少,那就是它执行的数组占11个字节。而 s 是8个字节,指向一个只读区,占 11 个字节。

练习

题目:分析以下示例。

#include int main() {    char str[20];    str = "HelloWorld";    char* s;    s = "HelloWorld";    // HelloWorld    printf("%s\n", s);    return 0;}

分析:

// 分配20个字节的内存,并把首地址给 strchar str[20];// str 是只读的,不能再赋值。报错:`error: array type 'char[20]' is not assignable`str = "HelloWorld";// 定义一个指针 schar* s;// 将 HelloWorld 的首地址给 ss = "HelloWorld";

扩展自定义strcpy()函数

题目:实现原生字符串拷贝方法strcpy。strcpy 其用法如下:

#include #include int main() {    char source[] = "Hello";    char destination[10]; // 目标字符串需要足够的空间来容纳 source 字符串    strcpy(destination, source);    printf("Source string: %s\n", source);    printf("Destination string: %s\n", destination);    return 0;}

实现:

#include char* strcpy_custom(char* destination, const char* source) {    // 字符串数组末尾有一个特殊的空字符 '\0' 来表示字符串的结束。逐个复制字符,直到遇到源字符串的结束标志 '\0'    while (*source != '\0') {        *destination = *source;        destination++;        source++;    }    *destination = '\0'; // 在目标字符串末尾添加结束标志 '\0'    return destination;}int main() {    // 定义两个字符数组    char source[] = "Hello";    char destination[10]; // 目标字符串需要足够的空间来容纳 source 字符串    // 数组名。表示首元素的地址,加 1 是加一个元素(比如这里1个字节)    strcpy_custom(destination, source);    printf("Source string: %s\n", source);    printf("Destination string: %s\n", destination);    return 0;}

Tipconst char* source 中 const 的作用请看const 和指针

输出:

开始运行...Source string: HelloDestination string: Hello运行结束。

将 while 替换成下面一行代码效果也相同:

char* strcpy_custom(char* destination, const char* source) {    /*    while (*source != '\0') {        *destination = *source;        destination++;        source++;    }    *destination = '\0';     */    // 替换成    while((*destination++ = *source++) != '\0');    return destination;}

分析:(*destination++ = *source++) != '\0':

之前的是首先判断,在赋值。`*source != '\0'`、`*destination = '\0';`,这里是先赋值后置++会放在表达式最后,所以等于:(*destination = *source) != '\0';destination++;source++;

const 和指针

首先补充下(int*)的作用。之前说到 const 定义的变量可以被修改,我们写了如下代码:

#include int main() {    const int val =5;    int *ptr= (int*)&val;    *ptr=10;    printf("val = %d\n",val);    printf("*ptr = %d\n", *ptr);    return 0;}

其中 int *ptr= (int*)&val; 是将一个 const int 类型的变量 val 地址强制转换为 int* 类型的指针,并将指针存储在 ptr 中。这种类型转换是不安全的,因为它丢失了 val 的常量性质。

const char* source 声明一个常量指针,以下代码仅做示意:

#include int main() {    const char* source = "Hello";    char* mutableSource = "World";    printf("%c\n", source[0]);    printf("%c\n", mutableSource[0]);    // 以下操作是非法的,会导致编译错误    // source[0] = 'h'; // 不能修改字符数据    // 合法    // 尽管mutableSource是一个非常量指针,看起来可以进行修改,但修改字符串常量是不被允许的,并且这可能导致未定义行为。    mutableSource[4] = 'w'; // 可以修改字符数据    return 0;}

运行:

开始运行...HWSegmentation fault (core dumped)运行结束。

就近原则

const 有个就近原则

  • 比如:const int* p1 = &num;,const 修饰的是 *,所以 *p1 不能修改, p1 可以修改
  • 比如:int* const p2 = &num;,const 修饰 p2,所以 p2 不能修改,*p2 可以修改

请看示例:

#include int main() {    int num = 1;    const int* p1 = &num; // const 修饰的是 *,所以 *p1 不能修改, p1 可以修改    p1++;    // (*p1)++;    int* const p2 = &num; // const 修饰 p2,所以 p2 不能修改,*p2 可以修改    // p2++;    (*p2)++;    const int* const p3 = &num; // 两个都不能修改    // p3++;    // (*p3)++;    return 0;}

gdb 调试段错误

GDB(GNU Debugger)是一款强大的调试器,用于帮助开发者查找和解决程序中的错误。通过与源代码交互,并提供诸如断点设置、变量观察、内存检查等功能,GDB允许开发者逐行执行程序并分析其运行状态。
除了上文使用的 run,还有如下操作

  • run:运行程序。
  • break :在指定行设置断点。
  • break :在指定函数设置断点。
  • continue:继续执行程序直到下一个断点或程序结束。
  • next:逐过程地执行程序。
  • step:逐语句地执行程序。
  • print :打印变量的值。
  • backtrace:显示函数调用的堆栈跟踪信息。
  • quit:退出GDB调试会话。

使用 gdb 调试段错误的过程如下:

编写代码:

pjl@pjl-pc:~/pjl$ cat demo-3.c#include int main() {    int* p;    *p = 1;    return 0;}

编译运行发现段错误:

pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3pjl@pjl-pc:~/pjl$ ./demo-3段错误 (核心已转储)

将代码编译为可调试的可执行文件。在gcc或g++编译时,添加”-g”选项可以生成包含调试信息的可执行文件。

// 增加 -gpjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3 -g// 启动GDB并加载可执行文件pjl@pjl-pc:~/pjl$ gdb demo-3GNU gdb (Ubuntu 9.1-0kylin1) 9.1Copyright (C) 2020 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:.Find the GDB manual and other documentation resources online at:    .For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from demo-3...(gdb) 

输入 run(还有其他操作) 找到是第5行代码报错:

...// run:运行程序。(gdb) runStarting program: /home/pjl/pjl/demo-3Program received signal SIGSEGV, Segmentation fault.0x0000555555555135 in main () at demo-3.c:55           *p = 1;(gdb)

高级指针

提前透露:指针遇上数组

题目:以下代码输出什么?

#include int main() {    char * string[] = {"Hello", "World" };    printf("%s\n", string);    return 0;}

分析:
我们知道定义字符串有以下两种方法:

char str[] = "HelloWorld";char* s = "HelloWorld";

Tip: string 在 C 中不是关键字,也不是保留字,就是一个普通变量名。

[] 的优先级是非常高的,这里首先是定义一个数组(string[]),其次就是指针,合起来就是一个指针数组。

首先在只读区分配两块内存分别存放 Hello(地址比如是 0x100) 和 World(地址比如是 0x200),指针数组是16个字节,本质就是数组,只不过里面放的是指针,比如前8个字节的地址是0x1000,那么 string 就是 0x1000,因为数组名就是数组首元素地址。

所以要输出这两个字符串,可以这么写:

#include int main() {    char * string[] = {"Hello", "World" };    // Hello    printf("%s\n", string[0]);    // World    printf("%s\n", string[1]);    return 0;}
作者:彭加李
出处:https://www.cnblogs.com/pengjiali/p/17502968.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。