初级指针
本篇主要介绍:指针和变量的关系、指针类型、指针的运算符、空指针和野指针、指针和数组、指针和字符串、const 和指针、以及gdb 调试段错误。
基础概念
指针是一种特殊的变量。存放地址的变量就是指针。
int num = 1; 会申请4个字节的内存来存放数字1,每次访问 num 就是访问这4个字节。
访问内存中的这4个字节,不仅可以通过名称(例如 num),还可以通过地址。
Tip:& 不仅是位运算符,还是取地址操作符。例如 int* ptr = #,就是取变量 num 的地址并将其保存到指针变量 ptr 中
请看示例:- #include <stdio.h>
- 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 的地址:0x7ffddcfe5328
- num 的地址加1 :0x7ffddcfe532c
- 10
复制代码 Tip: 指针 j 也有地址,也就是指针的指针。现在不研究
练习
题目:请问输出什么?- #include <stdio.h>
- 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 <stdio.h>
- 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 <stdio.h>
- 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类型指针所占字节数为:8
- short类型指针所占字节数为:8
- int类型指针所占字节数为:8
- long类型指针所占字节数为:8
- float类型指针所占字节数为:8
- double类型指针所占字节数为:8
- long long类型指针所占字节数为:8
复制代码 练习
题目:请问整数类型的指针和字符类型的指针加1分别是几个字节?- #include <stdio.h>
- 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 的地址:0x7fffe8244288
- num 的地址加1 :0x7fffe824428c
- ch 的地址:0x7fffe8244287
- ch 的地址加1 :0x7fffe8244288
复制代码 答案:int * 加1是4个字节;char * 加1是1个字节。&num 和 &ch 分别代表该变量的全部字节。
指针交换数据
比如这段代码是不能实现 a、b 两数交换。请看示例:- #include <stdio.h>
- 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;
- }
复制代码 分析:调用 swap(a, b) 这里是一个值传递,找到函数入口地址,对参数 x、y 申请空间和赋值,通过 tmp 变量完成了 x和y的交换,最后回收局部变量 x、y和tmp,释放空间。而 a,b数据没有变化。
可以通过指针来实现两数的交换。请看示例:- #include <stdio.h>
- 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;
- }
复制代码 分析:通过 swap(&a, &b) 将 a b 的地址传给 x 和 y,通过 x 和 y 指针对 a 和 b 进行交换,虽然最后会销毁swap中的局部变量,但 a 和 b的值已经完成了交换。
指针的运算符
指针和变量的关系
练习1
题目:输出什么?- #include <stdio.h>
- 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 也指向变量 a
- pb = pa;
- printf("%d\n", *pb);
- return 0;
复制代码 练习2
题目:输出什么?- #include <stdio.h>
- 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;
- }
复制代码 输出:- 8
- 4
- 0x7ffc48b9be38
- 0x7ffc48b9be3c
- 4
复制代码 分析:
- y= ++*px; 等效 ++(*px)。如果是 ++* 是不对的
- 类似 y = ++i,等于先执行 ++,在执行 y = i,
- 这里先对 (*px) 执行 ++,在返回 *px 的值
复制代码- 先执行 y = *px,然后是 px++。px是整数类型的地址,加1就是加4个字节。
复制代码 练习3
题目:输出什么?- #include <stdio.h>
- 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;
- }
复制代码 输出:- 0x7ffef1dc4d58
- 0x7ffef1dc4d58
- 4
复制代码 分析:*px++ 表示指针加1,(*px)++ 表示值加1。
指针初始化
指针初始化有两种方法:已经存在的空间和自己申请空间。
已经存在的空间,例如:- #include <stdio.h>
- #include <stdlib.h>
- int main() {
- int num;
- int* p = #
- *p = 10;
- char *str = "abc";
- printf("%s\n", str); // abc。把字符串的地址赋值给指针变量
- return 0;
- }
复制代码 自己申请空间可以使用 malloc 函数。申请的是 void 类型指针,也称为通用类型指针。请看示例:- #include <stdio.h>
- // malloc 需要引入 <stdlib.h>
- #include <stdlib.h>
- 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 <stdio.h>
- int main() {
- int* p;
- *p = 1;
- return 0;
- }
复制代码 这里声明一个指针 p,里面是一个随机数,例如 0x7ffe71df3f40,接着往指向的内存放1,由于这块内存不知道是否存在,即使存在也不能访问,于是报段错误。
直接手写一个地址也不可以。就像这样:- #include <stdio.h>
- 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 <stdio.h>
- 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 <stdio.h>
- 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 等于 *(pArr + i)。在这里[]不再是取某个索引,而是表示取值。
指针和字符数组
题目:分析 char a[] = "Hello"; 和 char *b = "World";
- #include <stdio.h>
- 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':
- H
- e
- l
- l
- o
- Characters in 'b':
- W
- o
- r
- l
- d
- 运行结束。
复制代码
- 为什么指针也可以通过索引访问特定字符?
比如 char *b = "World";,可以将字符串视为字符数组,使用指针来指向该数组的首地址,指针可以通过偏移来访问特定位置的元素,包括字符串中的字符。
练习
题目:下面代码中 p1[0]、p2[0]、p3[0]的值分别是多少?- // 申请4*4个字节,每个字节地址假如是:0x100(存放1) 0x104(存放2) 0x108 0x10c
- int 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 <stdio.h>
- 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 <stdio.h>
- 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 <stdio.h>
- 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 <stdio.h>
- int main() {
- char str[20];
- str = "HelloWorld";
- char* s;
- s = "HelloWorld";
- // HelloWorld
- printf("%s\n", s);
- return 0;
- }
复制代码 分析:- // 分配20个字节的内存,并把首地址给 str
- char str[20];
- // str 是只读的,不能再赋值。报错:`error: array type 'char[20]' is not assignable`
- str = "HelloWorld";
- // 定义一个指针 s
- char* s;
- // 将 HelloWorld 的首地址给 s
- s = "HelloWorld";
复制代码 扩展
自定义strcpy()函数
题目:实现原生字符串拷贝方法strcpy。strcpy 其用法如下:- #include <stdio.h>
- #include <string.h>
- 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 <stdio.h>
- 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;
- }
复制代码 Tip:const char* source 中 const 的作用请看const 和指针
输出:- 开始运行...
- Source string: Hello
- Destination 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 <stdio.h>
- 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 <stdio.h>
- 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;
- }
复制代码 运行:- 开始运行...
- H
- W
- Segmentation fault (core dumped)
- 运行结束。
复制代码 就近原则
const 有个就近原则:
- 比如:const int* p1 = #,const 修饰的是 *,所以 *p1 不能修改, p1 可以修改
- 比如:int* const p2 = #,const 修饰 p2,所以 p2 不能修改,*p2 可以修改
请看示例:- #include <stdio.h>
- int main() {
- int num = 1;
- const int* p1 = # // const 修饰的是 *,所以 *p1 不能修改, p1 可以修改
- p1++;
- // (*p1)++;
- int* const p2 = # // const 修饰 p2,所以 p2 不能修改,*p2 可以修改
- // p2++;
- (*p2)++;
- const int* const p3 = # // 两个都不能修改
- // 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 <stdio.h>
- int main() {
- int* p;
- *p = 1;
- return 0;
- }
复制代码 编译运行发现段错误:- pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3
- pjl@pjl-pc:~/pjl$ ./demo-3
- 段错误 (核心已转储)
复制代码 将代码编译为可调试的可执行文件。在gcc或g++编译时,添加"-g"选项可以生成包含调试信息的可执行文件。- // 增加 -g
- pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3 -g
- // 启动GDB并加载可执行文件
- pjl@pjl-pc:~/pjl$ gdb demo-3
- GNU gdb (Ubuntu 9.1-0kylin1) 9.1
- Copyright (C) 2020 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- 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:
- <http://www.gnu.org/software/gdb/bugs/>.
- Find the GDB manual and other documentation resources online at:
- <http://www.gnu.org/software/gdb/documentation/>.
- For help, type "help".
- Type "apropos word" to search for commands related to "word"...
- Reading symbols from demo-3...
- (gdb)
复制代码 输入 run(还有其他操作) 找到是第5行代码报错:- ...
- // run:运行程序。
- (gdb) run
- Starting program: /home/pjl/pjl/demo-3
- Program received signal SIGSEGV, Segmentation fault.
- 0x0000555555555135 in main () at demo-3.c:5
- 5 *p = 1;
- (gdb)
复制代码 高级指针
提前透露:指针遇上数组
题目:以下代码输出什么?- #include <stdio.h>
- 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 <stdio.h>
- int main() {
- char * string[] = {"Hello", "World" };
- // Hello
- printf("%s\n", string[0]);
- // World
- printf("%s\n", string[1]);
- return 0;
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |