系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 机器学习入门之pandas的使用
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 系列文章目录
- 前言
- 一、基本概念
-
- [1.1 作用](#1.1 作用)
- [1.2 内存、地址、变量](#1.2 内存、地址、变量)
- [1.3 指针和指针变量](#1.3 指针和指针变量)
- [1.4 指针的目标和解引用](#1.4 指针的目标和解引用)
- [1.5 指针的赋值](#1.5 指针的赋值)
- [1.6 指针的大小](#1.6 指针的大小)
- [1.7 空指针](#1.7 空指针)
- [1.8 野指针](#1.8 野指针)
- 二、指针运算
-
- [2.1 算术运算](#2.1 算术运算)
-
- [指针 ± 整数](#指针 ± 整数)
- 指针相减
- 自增/自减的经典组合
- 大小端模式
- [2.2 指针关系运算](#2.2 指针关系运算)
- 三、指针与数组
-
- [3.1 指针与一维数组](#3.1 指针与一维数组)
- [3.2 二维数组](#3.2 二维数组)
- [3.2 字符指针与字符串](#3.2 字符指针与字符串)
- [3.3 指针数组](#3.3 指针数组)
- 四、多级指针
- [void 指针](#void 指针)
- [const 指针](#const 指针)
- [main 函数参数](#main 函数参数)
-
- [1. 函数指针](#1. 函数指针)
- [2. 动态内存分配](#2. 动态内存分配)
- [3. 指针的指针与二维数组的动态分配](#3. 指针的指针与二维数组的动态分配)
- [4. 常见错误与陷阱](#4. 常见错误与陷阱)
前言
提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。
提示:以下是本篇文章正文内容,下面案例可供参考
以下是对你指针学习笔记的全面补充与修正 。我保留了原有结构,更正了错误,澄清了模糊之处,并补充了缺失的重要知识点(如指针与 const 的更多细节、函数指针、指针与数组的更多区别等)。修改处已用 【修正】 或 【补充】 标记,方便你对照。
一、基本概念
1.1 作用
- 使程序简洁、紧凑、高效
- 有效地表示复杂的数据结构(如链表、树、图)
- 动态分配内存
- 能直接访问硬件(如内存映射寄存器)
- 能够方便地处理字符串
- 得到多于一个的函数返回值(通过指针参数)
1.2 内存、地址、变量
-
内存【修正】
-
程序和数据永久存储在硬盘等非易失性存储器上,关机后不丢失。
-
运行程序时,程序和数据必须从硬盘加载到内存,CPU 再从内存中读取指令和数据。
-
CPU 直接与内存交互,处理完成后将结果写回内存(或缓存)。

-
-
内存地址【补充】
-
内存以字节 为基本单位,每个字节都有一个唯一的编号,即内存地址。
-
32 位处理器有 32 根地址线,可寻址
2^32字节 = 4 GB。 -
64 位处理器通常支持
2^48或2^52字节(实际未用满 64 位)。内存被划分成一个个小的单元格,每个单元格表示1个bit,只能存放0或1两种状态。以8bit为一组表示1个byte,并且将1byte大小作为内存操作的基本单元,对基本单元进行编址------内存地址。所有编号连起来就叫做内存的地址空间。
-
-
变量
- 变量是内存中某段区域的名字 ,用来存储数据。例如
int k = 58;分配 4 个字节,k代表这段空间,也代表其中存储的值。
- 变量是内存中某段区域的名字 ,用来存储数据。例如
-
变量和地址
- 变量
k的地址是它占用的内存区域的首字节地址。例如,CPU为k分配4个字节0x0012FF02~0x0012FF05。0x0012FF02就作为变量k的地址。 - 变量名在编译后被替换为地址,因此 CPU 只认地址,不认变量名。

- 变量
-
一切皆地址【补充概念】
- 数据和代码都以二进制存储在内存中。操作系统通过内存权限区分:可读可执行的是代码段,可读可写的是数据段。当程序被加载到内存的过程中,就将各种内容加载到对应的内存块中。
- CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
- 由于通过地址能找到所需的内存单元,可以说地址指向该内存单元。因此,将地址形象化的称为"指针"
1.3 指针和指针变量
定义
- 指针 = 内存地址(一个无符号整数,是常量)。
- 指针变量 = 存放地址的变量(内容可变)。
指针变量的一般形式
c
<数据类型> *<指针变量名>;
int *p; // p 是一个指针变量,可以存放 int 类型变量的地址
*表示这是一个指针类型。- 数据类型决定了指针解引用时访问的字节数(步长)。例如
int *步长 4(通常),char *步长 1。 - 一般数据类型要和这个地址中保存的数据的数据类型保持一致。
初始化
c
<数据类型> *<指针变量名> = <地址量> ;
<数据类型> *<指针变量名> = NULL ;
int a = -126; ////在内存中开辟一块内存空间
int *p = &a; // & 取地址运算符,取出变量a的起始地址
- 不能直接赋值一个普通整数(0 除外),因为类型不匹配。而是通过取地址符将某个变量的起始地址拿过来赋值。
- 可以初始化为
NULL。

1.4 指针的目标和解引用
-
指针的目标 :指针所指向的内存区域中的数据 (即变量本身)。目标是一个左值(可以赋值)。
cint a = 126; int *p = &a; // p 的目标是变量 a
-
解引用 :通过
*p访问指针指向的目标。*p等价于a。对*p进行加减乘除,也就是对a进行加减乘除,所以也称为"间接访问"cint a = 126; int *p = &a; (*p)++; -
类型决定步长 :
int *p解引用访问 4 字节;char *p访问 1 字节。cint a = 0x11111111; int *p = &a; *p = 0x22222222; printf("%p\n",p); printf("%x\n",a); //改变4字节 char *p1 = (char *)&a; *p1 = 0x33333333; printf("%p\n",p); printf("%x\n",a); //只改变了1字节 short *p2 = (short *)&a; *p2 = 0x44444444; printf("%p\n",p); printf("%x\n",a); //只改变了2字节
区别三种形式
c
p // 指针变量本身,内容是地址(右值)
*p // 指针的目标,内容是数据(左值)
&p // 指针变量的地址(右值,常量)
*(&p) // 等价于 p, *和&两个运算符连用时,互相抵消

1.5 指针的赋值
- 必须赋地址值 (如
&a、数组名、函数名、另一个同类型指针、NULL)。 - 为什么普通整数不能直接赋给指针?
虽然地址本质是整数,但 C 语言要求类型匹配。int *p = 100;类型不匹配,编译器报错或警告。
可以用强制转换(int *)100,但通常危险(除非你知道 100 是有效地址)。 - 赋 0 是例外 :
int *p = 0;表示空指针(NULL),标准允许整数字面量0可以隐式转换为任意指针类型,得到空指针。#define NULL ((void*)0)
常见赋值形式:注意类型相同
c
double x=15, *px;
px = &x; // ① 普通变量地址
float a, *px, *py;
px = &a;
py = px; // ② 指针变量赋值
int a[20], *pa;
pa = a; // ③ 数组名赋值(等价 &a[0])
1.6 指针的大小
-
32 位系统:指针变量占 4 字节。
-
64 位系统:指针变量占 8 字节。
-
与指针指向的类型无关,与计算机地址线数量有关。
-
查看系统位数:
uname -m(Linux)或getconf LONG_BIT。cprintf("%zu\n",sizeof(char *)); printf("%zu\n",sizeof(short *)); printf("%zu\n",sizeof(int *)); printf("%zu\n",sizeof(float *)); printf("%zu\n",sizeof(double *));对于32位的系统,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平和低电平。那么32根地址线产生的地址就是32个(1或者0)`2^32Byte = 4GB`,共 4G的空闲进行编址。同样的方法,那64位机器给64根地址线,`2^64Byte` 。
1.7 空指针
NULL通常定义为(void *)0或直接0。- 空指针不指向任何有效内存,解引用会导致段错误(Segmentation Fault)。
- 使用前应检查
if (p != NULL)。 - 为什么指向0地址处?2个原因。
① 指针如果没有给定初始值,值是不确定的
② 0地址是一个特殊地址,在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误
1.8 野指针
- 定义:指向非法或已释放内存的指针。
- 危害:段错误(指向不可访问地址)、数据污染、难以调试的随机错误(指向可用且其他程序正在使用的地址)、错误被掩盖(指向可用且未被使用的空间,错误被掩盖)。
- 规避方法 :
- 定义时初始化为
NULL。 - 使用前确保指向有效内存(如
malloc、取地址)。 - 解引用前检查
if (p != NULL)。 - 避免指针越界。
- 释放内存后将指针置为
NULL。 - 不要返回局部变量的地址(函数返回后局部变量被销毁)。
- 定义时初始化为
正确示范:
```c
#include <stdio.h>
int main(){
int a = 12,b = 23;
int t;
int *p = &a;
int *q = NULL;
printf("before: a = %d,b = %d\n",a,b);
q = &b;
if(p != NULL && q != NULL){
t = *p;
*p = *q;
*q = t;
}
printf("after: a = %d,b = %d\n",a,b);
return 0;
}
```
错误示范:
```c
int a = 10;
int *p = NULL;
if(a>10){
int b = 20;
p = &b;
a += *p;
}
//(*p)++; //野指针,出了if语句,b已经被销毁,p指向未定义空间,不该再使用了
p = NULL;
```
二、指针运算
l 指针运算是以指针所存放的地址作为运算量而进行的
l 指针运算的实质就是地址的计算
2.1 算术运算
包括:指针 ± 整数、自增/自减、指针相减。

指针 ± 整数
-
p + n的地址 =p的地址 +n * sizeof(指向的类型)。 -
p++移动一个步长(即sizeof(*p))。 -
不同数据类型的指针加减整数是有意义的 (分别移动各自的步长),但两个不同类型的指针相减 无意义(编译器报错)。
c//p+n #include <stdio.h> int main(){ int a = 10; int *p = &a; printf("%p\n",p); printf("%p\n",p+1); char c = 'a'; char *pc = &c; printf("%p\n",pc); printf("%p\n",pc+1); return 0; }
指针相减
-
p - q结果是一个整数(ptrdiff_t),表示两个指针之间相隔的元素个数(要求指向同一数组或其后一位置)。 -
两个指针相加是非法的。
-
技巧:对于一段连续的空间,如果想要挨个字节访问,利用char *p指向首地址,p++逐个访问。同理,挨个整形访问,就用int *p。
c#include <stdio.h> int main(){ int arr[] = {1,2,3,4,5,6,7,8}; int *p = arr; printf("%d\n",*(p += 3)); printf("%d\n",*(p -= 1)); int *q = &arr[7]; printf("%ld\n",q - p); printf("%ld\n",p - q); return 0; }
自增/自减的经典组合
c
int a[] = {6, 8, 10};
int *p = a;
*p++; //什么改变了,什么没变
++的优先级高于*,p先与++结合,后置运算返回原先的p,p解引用,最后p++。

*p++、(*p)++、++*p 、*++p 执行顺序,例:
c
#include <stdio.h>
int main(){
int arr[4] = {1,2,3,4};
int *p = arr;
printf("%d\n",*p++); //1
printf("%d\n",*p); //2
for(int i = 0; i < 4; i++){
printf("%d ",arr[i]); //1 2 3 4
}
putchar('\n');
//初始化p指向数组的第一个元素 arr[0](值为 1)。
//*p++:后缀 ++ 优先级高于 *,所以先执行 p++,但后缀自增返回的是 p 原来的值,然后 p 自增 1。因此 *p++ 等价于 *(p++),先取出 p 原来指向的值arr[0],第一个 printf 输出 1。然后 p 指向下一个元素 arr[1],第二个 printf 输出2
p = arr; //恢复原样
printf("%d\n",(*p)++); // 1
printf("%d\n",*p); // 2
for(int i = 0; i < 4; i++){
printf("%d ",arr[i]); // 2 2 3 4
}
putchar('\n');
//初始状态:p 指向 arr[0],arr[0] 的值为 1。
//(*p)++:括号使 *p 先被取出,得到值 1,然后对该值(即 arr[0])执行后缀自增,返回(*p)原值,第一个printf打印 1 ,接着该值++,因此 arr[0] 变为 2。接着 printf("%d\n", *p);:此时 p 仍然指向 arr[0],而 arr[0] 已经变成了 2,所以输出 2。
arr[0] = 1; //恢复原样
printf("%d\n",++*p); // 2
printf("%d\n",*p); // 2
for(int i = 0; i < 4; i++){
printf("%d ",arr[i]); // 2 2 3 4
}
putchar('\n');
//初始时 p 指向 arr[0],arr[0] 的值为 1。
//++*p:前缀++和*优先级相同,且是右结合,所以等价于 ++(*p)。先取 *p(值为 1),然后执行前缀自增,将 arr[0] 变为 2,并返回自增后的值 2。因此第一个 printf 输出 2。
//此时 p 仍指向 arr[0],而 arr[0] 已经是 2,所以第二个 printf 输出 2
arr[0] = 1; //恢复原样
printf("%d\n",*++p); //2
printf("%d\n",*p); //2
for(int i = 0; i < 4; i++){
printf("%d ",arr[i]); // 1 2 3 4
}
putchar('\n');
//p 初始指向 arr[0](值为 1)时:
//*++p:前缀 ++ 与 * 优先级相同,右结合,等价于 *(++p)。先执行 ++p,使 p 指向 arr[1](值为 2),再解引用得到 2。第一个 printf 输出 2。
//此时 p 已指向 arr[1],第二个 printf 输出 *p 的值,即 2。
return 0;
}
以上内容换成 --也是同样逻辑,只是方向相反
总结
-
后置 运算符(
++,--)先返回变量原数据,是右值(不能赋值),表达式使用原数据执行完后,变量自加/减1(变量的加/减1操作在表达式之后,分号之前)后置(
++,--)优先级高于* -
前置 运算符(
++,--)变量先自加/减1(变量的加/减1操作在表达式之前),自加/减后将变量返回给表达式,是左值(可以赋值,但极少使用)前置(
++,--)优先级与*相同,结合方式为右到左*p的结果是左值(可以赋值)。 -
*p++和(*p)++*p++;指针移动,原目标不变
*p++ = 3;指针移动,原目标变为3
(*p)++;指针不移动,原目标+ 1
(*p)++ = 3;语法错误,(*p)++返回右值(后缀自增/减返回右值) -
*++p和++*p*++p;指针移动,原目标不变
*++p = 3;指针移动,移动后指向的目标变为3
++*p;指针不移动,源目标+ 1
++*p = 3;指针不移动,++*p是左值,前缀自增/减返回左值。++*p = 3等价于++(*p); *p = 3;最终*p被赋值为3(先自增再覆盖)。 -
*p++中的p++副作用(指针移动)会在整个表达式求值结束后的"序列点"(如语句结束的分号)之前完成,但表达式的值使用的是原指针。 -
像
*p++ = 3 或 ++*p = 3虽然语法正确,但阅读困难。建议拆成多行代码,提高可维护性。cint main() { int a[] = {5, 8, 7, 6, 2, 7, 3}; int y, *p = &a[1]; y = (*--p)++; printf("%d ", y); printf("%d", a[0]); } 输出:5 6
大小端模式
大小端指的是在存储器中,存放数据的字节顺序
- 小端:低字节存放在低地址(x86、ARM 常用)。
- 大端:低字节存放在高地址(网络字节序、某些嵌入式)。
unsigned int value = 0x12345678

写一段代码判断机器是大端模式还是小端模式
c
include <stdio.h>
int main(){
unsigned int num = 0x11223344;
char* p = (char *)#
printf("%x\n",num);
printf("%x",*p);
p++;
printf("%x",*p);
p++;
printf("%x",*p);
p++;
printf("%x\n",*p);
if(*p == 0x11){
printf("little-endian\n");
}else{
printf("big-endian\n");
}
return 0;
}
2.2 指针关系运算

两指针之间的关系运算表示它们指向的地址位置之间的关系。
- 可以比较两个指针(
<,<=,>,>=,==,!=),前提是指向同一数组或其后一位置。 - 不能与指向数组第一个元素之前的位置比较(标准未定义)。

P1可以与p2比较,不能与p3比较 - 可与
NULL比较。判断指针是否为空。
注意 :指针可以指向数组最后一个元素的下一个位置(用于循环终止),但不能解引用。
例:用指针实现逆序存放数组元素值
c
#include <stdio.h>
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9};
int size = sizeof(arr) / sizeof(int);
int *p = arr,*q = &arr[size-1];
int n;
while(p < q){
n = *p;
*p = *q;
*q = n;
p++;
q--;
}
p = arr;
for(int i = 0; i < size; i++){
printf("%d ",*p++);
}
putchar('\n');
p = NULL;
return 0;
}
c
#include <stdio.h>
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9};
int size = sizeof(arr) / sizeof(int);
int *p = arr;
int n;
for(int i = 0; i < size/2; i++){
n = *(p+i);
*(p+i) = *(p+size-1 - i);
*(p+size-1-i) = n;
}
p = arr;
for(int i = 0; i < size; i++){
printf("%d ",*p++);
}
putchar('\n');
p = NULL;
return 0;
}
三、指针与数组
数组指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的起始地址
3.1 指针与一维数组
- 数组名是一维数组的指针(起始地址),是地址常量 (右值),不能自增/自减。如:
int a[10];

a[i]、*(a+i)、p[i]、*(p+i)等价(当p = a时)。

sizeof(a)是整个数组的字节数;sizeof(p)是指针本身的字节数(4/8)。p[-1]可能合法(如果p指向非首元素),但a[-1]永远越界(未定义行为)。
数组名与指针的区别
在p=a的条件下,指针变量和数组在访问数组中元素时有相同的形式。
- 含义不同
- 数组名代表一个数组,存放相同类型的元素
- 指针代表存储地址的变量
- 使用不同
- 数组名不是左值,不能出现在赋值左侧。
- 指针是变量,可以赋值、自增。
- 数组名在
sizeof(a)和&a时,a代表的对象是整个数组 ,其他情况a代表的对象是数组首元素地址
c
include <stdio.h>
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9};
int size = sizeof(arr) / sizeof(int);
int *p = arr;
printf("%d %p\n",*p,p);
p = &arr[2]; //p = arr + 2; p += 2;
printf("%d %p\n",*p,p);
// arr = arr + 2;
printf("sizeof(arr) = %ld\n",sizeof(arr)); //40
printf("sizeof(p) = %ld\n",sizeof(p)); //4 / 8
//p[i]与arr[i]相等的前提是p = arr
printf("%d %d %d %d\n",arr[1],*(arr+1),p[1],*(p+1));
printf("%d\n",p[-1]); //当p指向arr[0]之后时,合法,且不越界;当p指向arr[0],即p=arr时,越界
printf("%d\n",arr[-1]); //越界
return 0;
}
3.2 二维数组
一级指针遍历二维数组
二维数组的元素连续存储,按行优先存

- 将二维数组视为一维连续空间:
int *p = &a[0][0];然后p[i]访问。
c
#include <stdio.h>
int main() {
int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
int i, j;
int n = sizeof(a) / sizeof(a[0]);
int m = sizeof(a[0]) / sizeof(int);
int *p;
for (i = 0; i < n; i++) {
for (j = 0; j < m; j++) {
//printf("%d ", a[i][j]);
printf("%p ", &a[i][j]); //观察地址
}
putchar('\n');
}
//一级指针遍历二维数组
p = &a[0][0];
for (i = 0; i < n * m; i++) {
printf("%d %d\n", *(p+i), p[i]);
}
return 0;
}
数组指针(行指针)
把二维数组看作由多个一维数组构成。
int a[3][3],含有三个元素:a[0]、a[1]、a[2];元素a[0]、a[1]、a[2]都是一维数组名
- 定义:
int (*p)[列数];指向一维数组的指针。p叫做行指针变量。 - 二维数组名是行指针(数组指针)常量,
a的类型为int (*)[列数]。 a+1移动一整行。

补充:二维数组的几种访问方式
a[i][j]*(a[i] + j)*(*(a + i) + j)(*(a+i))[j]
例:行指针大小
c
#include <stdio.h>
int main() {
int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
int (*p)[4] = a;
printf("sizeof(a)=%ld\n", sizeof(a));
printf("sizeof(a[0])=%ld\n", sizeof(a[0]));
printf("sizeof(a[1])=%ld\n", sizeof(a[1]));
printf("sizeof(a[2])=%ld\n", sizeof(a[2]));
printf("%p %p\n", a, p);
printf("%p %p\n", a + 1, p + 1);
printf("%p %p\n", a + 2, p + 2);
return 0;
}
例:行指针遍历二维数组
c
#include <stdio.h>
int main() {
int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
int i, j;
int n = sizeof(a) / sizeof(a[0]);
int m = sizeof(a[0]) / sizeof(int);
int (*p)[4];
for (i = 0; i < n; i++) {
for (j = 0; j < m; j++) {
printf("%d ", a[i][j]);
printf("%d ", *(a[i] + j));
printf("%d ", *(*(a + i) + j));
}
putchar('\n');
}
p = a;
for (i = 0; i < n; i++) {
for(j = 0; j < m; j++){
printf("%d \n", *(p[i] + j));
printf("%d \n", *(*(p + i) + j));
printf("%d \n", p[i][j]);
}
}
return 0;
}
练习题
c
int a[3][4] = { 0 };
printf("%ld\n", sizeof(a[0]));
printf("%ld\n", sizeof(*a));
printf("%ld\n", sizeof(*(a + 1)));
printf("%ld\n\n", sizeof(*(&a[0] + 1)));
printf("%ld\n", sizeof(a + 1));
printf("%ld\n", sizeof(&a[0] + 1));
printf("%ld\n\n", sizeof(a[0] + 1));
printf("%ld\n\n", sizeof(*(a[0] + 1)));
c
int a[] = { 1, 2, 3, 4 };
printf("%ld\n", sizeof(&a));
printf("%ld\n", sizeof(*&a));
printf("%ld\n", sizeof(&a + 1));
c
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d",*(a + 1),*(ptr - 1));
c
int a[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)(*(a + 1));
printf("%d,%d",*(ptr1 - 1),*(ptr2 - 1));
c
int a[5][5];
int(*p)[4];
p = a;
printf("%d\n", &p[2][1] - &a[2][1]);
3.2 字符指针与字符串
-
C使用字符数组来处理字符串
-
char数据类型的指针变量称为字符指针变量
-
字符指针变量与字符数组有着密切关系,它也被用来处理字符串
-
初始化字符指针是把内存中字符串的首地址赋予指针,并不是把该字符串复制到指针中
-
char *p = "Hello";指向字符串常量,不能修改 内容(*p = 'h'错误)。 -
char s[] = "Hello";是字符数组,可以修改。 -
char *p = s;指向字符数组,可用修改(*p = 'h') -
字符串长度
strlen不包括'\0'。 -
字符指针可以遍历字符串,注意不要越界。
-
char s[] ="hello"和char *s ="hello"有什么区别访问类似,但本质完全不同
指针和数组名都表示首元素地址
数组名是地址常量(右值)不能修改,指向的对象可以修改。指针可以被修改,指针指向的对象不能修改
例:对于一个字符串,进行大小写字母转换
c
#include <stdio.h>
int main() {
char s[] = "How Are You! ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz";
char *p = s;
printf("%s\n",s);
while(*p != '\0'){
if(*p >= 'A' && *p <= 'Z'){
*p += 32;
}else if(*p >= 'a' && *p <= 'z'){
*p -= 32;
}
p++;
}
printf("%s\n",s);
return 0;
}
例:不利用任何字符串函数,编程实现字符串连接的功能。
c
#include <stdio.h>
int main() {
char s1[100] = "abcd";
char *s2 = "EFGH";
int i = 0;
while(*s2 != '\0'){
while(s1[i] != '\0'){
i++;
}
s1[i] = *s2;
s2++;
}
s1[i+1] = '\0';
printf("%s\n",s1);
/*
char s1[100] = "abcd";
char *s2 = "EFGH";
char *p = s1,*q = s2;
while(*p != '\0'){
p++;
}
while(*q != '\0'){
*p = *q;
p++;
q++;
}
*p = '\0';
printf("%s\n",s1);
*/
return 0;
}
3.3 指针数组
由若干个具有相同数据类型的指针变量构成的集合
-
定义:
c<数据类型> *<指针数组名>[<大小>];int *p[5];数组,每个元素是int *。数组名p是指针数组的起始地址 -
常用于存储多个字符串(
char *s[] = {"abc","def"};)。 -
指针数组名是二级指针常量(
char **类型)。 -
指针数组名加1,移动多少字节?32位移动4字节,64位移动8字节
c
#include <stdio.h>
int main() {
int a = 10, b = 20, c = 20;
int * p[3];
p[0] = &a;
p[1] = &b;
p[2] = &c;
printf("a-b-c:%d %d %d\n", a, b, c);
printf("%d %d %d\n", *p[0], *p[1], *p[2]);
printf("%p %p %p\n", &a, &b, &c);
printf("%p %p %p\n", p[0], p[1], p[2]);
printf("sizeof:%ld\n", sizeof(p)); //12
printf("%p %p\n", p, p + 1);
return 0;
}
c
#include <stdio.h>
int main() {
int a[] = {5, 8, 2, 5, 9, 10};
int * p[6];
int i;
for (i = 0; i < sizeof(a) / sizeof(int); i++) {
p[i] = &a[i];
}
for (i = 0; i < sizeof(a) / sizeof(int); i++) {
printf("%d\n", *p[i]);
}
return 0;
}
c
#include <stdio.h>
int main() {
char * s[] = {"BeiJing", "ShangHai", "GuangZhou"};
int i;
for (i = 0; i < sizeof(s) / sizeof(char *); i++) {
printf("%s\n", s[i]);
}
return 0;
}
指针数组与二维数组
• 声明一个指针数组:
c
int * p[2] ,a[2][3];
• 把一维数组a[0]和a[1]的首地址分别赋予指针变量数组的数组元数p[0]和p[1]:
c
p[0]=a[0];
p[1]=a[1];

指针数组遍历二维数组
c
#include <stdio.h>
int main() {
int a[2][3] = {{3, 5, 1}, {3, 6, 9}};
int * p[2];
int i, j;
p[0] = a[0];//&a[0][0];
p[1] = a[1];//&a[1][0];
for (i = 0; i < 2; i++) {
for (j = 0; j < 3; j++) {
//printf("%d ", a[i][j]);
printf("%d %d ", *(p[i] + j), p[i][j]);
}
printf("\n");
}
return 0;
}
数组指针 vs 指针数组
int *p[5]:- 指针数组,本质是数组(元素是指针的一维数组)
- 指针数组经常结合二维数组使用,存储每行首元素的地址
int (*p)[5]:- 数组指针,本质是指针(指向有5个int的一维数组)
- 一维数组名取地址,或者二维数组名,都可以赋给它
四、多级指针
把一个指向指针变量的指针变量,称为多级指针变量
• 一级指针变量:指向处理数据的指针变量
• 二级指针变量:指向一级指针变量的指针变量
二级指针变量的说明形式如下
c
<数据类型> ** <指针名> ;
int **p:指向int *的指针。- 多级指针常用于函数参数(传递指针的地址)或动态二维数组。
- 运算步长:
p+1移动一个int *的大小(4/8)。
c
#include <stdio.h>
int main() {
int a = 10;
int *p = &a;
int **q = &p;
printf("%d %p %p\n",a,p,q);
printf("%p %p %p\n",&a,&p,&q);
printf("%ld %ld %ld\n",sizeof(a),sizeof(p),sizeof(q));
printf("%d %d %d\n",a,*p,**q);
return 0;
}

多级指针的运算
- 指针变量加1,是向地址大的方向移动一个目标数据
- 多级指针运算也是以其目标变量为单位进行偏移
int **p;p+1移动一个int *变量所占的内存空间(4/8)
int ***p,p+1移动一个int **所占的内存空间(4/8)
c
#include <stdio.h>
int main() {
int a = 10;
int *p = &a;
int **q = &p;
int ***g = &q;
printf("int *:%p %p\n",p,p+1);
printf("int **:%p %p\n",q,q+1);
printf("int ***:%p %p\n",g,g+1);
return 0;
}
int *:0x7ffc3d64d20c 0x7ffc3d64d210
int **:0x7ffc3d64d210 0x7ffc3d64d218
int ***:0x7ffc3d64d218 0x7ffc3d64d220
多级指针与数组
指针数组也可以用另外一个指针来处理。
例如:有一个一维字符指针数组ps[5],
c
char *ps[5]= { "Beijing city", ......"London city" } ;
定义另一个指针变量pps,并且把指针数组的首地址赋予指针pps
c
char *ps[5]={......}; char ** pps = ps;

由于ps是常量,不能自增/减,利用一个二级指针指向,达到灵活运用的效果
c
#include <stdio.h>
int main() {
char *ps[5] = {"Beijing","Moacow","NewYork","Paris","London"};
char **pps = ps;
printf(" %s\n %s\n %s\n\n",ps[1],pps[1],*(pps + 1));
for(int i = 0; i < 5; i++){
printf("%s\n",*pps++);
}
return 0;
}
c
#include <stdio.h>
int main() {
int a[] = {1,2,3,4,5};
int n = sizeof(a) / sizeof(int);
int *p[5];
int **q;
for(int i = 0; i < n; i++){
p[i] = &a[i];
}
for(int i = 0; i < n; i++ ){
printf("%d %d\n",a[i],*p[i]);
}
q = p; //&p[0]
printf("%d %d %d %d\n",a[1],*p[1],**(q+1),*q[1]);
return 0;
}

补充:二级指针与指针数组的关系
c
char *arr[] = {"a","b","c"};
char **p = arr; // p 指向第一个指针元素
void 指针
万能指针 其实就是void *类型的指针,而void *指针一般被称为通用指针 或叫泛指针 。
一般形式为:
c
void * <指针变量名称> ;
void *p可以指向任何类型,但不能直接解引用或算术运算(因为不知道步长)。- 使用前必须被初始化,使用时需要强制转换:
*(int *)p。 - 典型应用:
malloc返回值、函数参数(如qsort、pthread_create)。增大函数的适用范围
c
#include <stdio.h>
int main() {
int a = 10;
float b = 3.14;
char c = 'c';
void * p;
p = &a;
printf("int:%d %d\n", a, *(int *)p);
p = &b;
printf("float:%f %f\n", b, *(float *)p);
p = &c;
printf("char:%c %c\n", c, *(char *)p);
return 0;
}
const 指针
- 指向常量的指针 :
const int *p;或int const *p;
不能通过*p修改目标,但p本身可以指向别处。 - 指针常量 :
int * const p;
p的指向不可变,但可以通过*p修改目标。 - 指向常量的指针常量 :
const int * const p;
两者都不可变。
注意 :const 放在 * 左边修饰指向的内容,右边修饰指针本身。
main 函数参数
c
int main(int argc, char *argv[]) // 或 char **argv
argc:命令行参数个数(包括程序名)。argv[0]:程序名。argv[1]到argv[argc-1]是命令行参数(命令行中各字符串的首地址)。argv[argc]为NULL。
1. 函数指针
-
定义:
int (*func_ptr)(int, int); -
指向函数的指针,可用于回调函数、动态调用。
-
示例:
cint add(int a, int b) { return a+b; } int (*p)(int,int) = add; int c = p(2,3); // 调用
2. 动态内存分配
malloc,calloc,realloc,free- 必须检查返回值是否为
NULL。 - 避免内存泄漏:配对
free。
3. 指针的指针与二维数组的动态分配
c
int **matrix = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
matrix[i] = (int*)malloc(cols * sizeof(int));
4. 常见错误与陷阱
- 使用未初始化的指针。
- 返回局部变量的地址。
- 指针越界。
- 忘记分配内存就直接解引用。
- 内存泄漏(丢失
free的机会)。