指针
指针和内存单元
- 指针
- 地址
- 内存单元
- 计算机中最小的存储单元,大小为一个字节
- 每个内存单元都有一个唯一编号
- 指针变量
- 存地址的变量
- 指针是一种数据类型,占用内存空间,用来保存内存地址
指针的定义和使用
- 指针的写法
// 以下写法都对 int* p; // Windows 写法 int *p; // Linux 写法 int * p; // 也可以 int a, *p, *q, b;
- 指针的解引用 (间接引用)
将 p 变量的内容取出,当成地址看待,找到该地址对应的内存空间。
*p = 250;
- *p 如果做左值,存数据到空间中
- *p 如果做右值,取出空间中的内容
- 任意指针类型大小
指针大小与类型无关,只与当前使用的平台架构有关:
- 32 位:4 字节
- 64 位:8 字节
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int a = 10; int *p = &a; *p = 30; printf("a = %d\n", a); printf("p = %p\n", p); printf("*p = %d\n", *p); printf("int * = %u\n", sizeof(int*)); printf("short * = %u\n", sizeof(short*)); printf("char * = %u\n", sizeof(char*)); printf("long * = %u\n", sizeof(long*)); printf("double * = %u\n", sizeof(double*)); system("pause"); return 0; } /* a = 30 p = 008FFAB8 * p = 30 int* = 4 short* = 4 char* = 4 long* = 4 double* = 4 */
- *p 类似于链接,记录的是 a 的地址,a 值变了, *p 也就变了
- 打印地址使用 %p
空指针和野指针
- 空指针
空指针不指向任何东西,常用来判断存储空间是否为空。
int *p = NULL;
Note: *p 是 p 所对应的存储空间,一定是一个无效的访问区域。
- 在解引用之前,要确保指针非 NULL
-
不能向 NULL 或非法内存拷贝数据
-
示例
#define _CRT_SECURE_NO_WARNINGS #include int main(void) { int* p = NULL; if (p != NULL) { printf("*p = %d\n", *p); } system("pause"); return 0; }
- 野指针
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。
- 出现野指针的情况
#define _CRT_SECURE_NO_WARNINGS #include #include void test0301() { // 指针变量未初始化 //int* p; //printf("%d\n", *p); // error C4700: 使用了未初始化的局部变量“p” // 指针释放后未置 NULL char* p = malloc(100); free(p); } char *test0302() { int a = 10; int *p = &a; *p = 20; return p; } char test0303() { int a = 10; int* p = &a; *p = 20; return *p; // 指针操作超越了变量的作用域 } int main(void) { //test0301(); printf("%d\n", *test0302()); printf("%d\n", test0303()); // 20 system("pause"); return 0; }
- 指针变量未初始化
- 未初始化的指针变量指向一个随机值
- 指针释放后未置 NULL
- 指针在 free 或 delete 后未赋值 NULL,它们只是把指针所指的内存释放掉了,但并没有把指针本身干掉,此时,指针指向垃圾内存
-
空指针可以重复释放,野指针不可以重复释放
free(p); p = NULL;
- 指针操作超越了变量的作用域
- 不要返回指向栈内存的指针或引用,因为栈内存在函数结束后会被释放
- 指针变量未初始化
- 避免野指针出现
- 初始化时置 NULL
- 释放时置 NULL
- 示例
- 没有一个有效的地址空间的指针
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int* p; *p = 2000; printf("*p = %d\n", *p); system("pause"); return 0; }
- 报错:使用未初始化的内存 “p”
- 报错:使用未初始化的内存 “p”
- p 变量有一个值,但该值不是可访问的内存区域
#define _CRT_SECURE_NO_WARNINGS #include #include // 野指针2 int main(void) { int *p = 100; *p = 2000; printf("*p = %d\n", *p); system("pause"); return 0; }
- 报错:引发了异常,写入访问权限冲突
-
合理做法
#define _CRT_SECURE_NO_WARNINGS #include #include // 野指针2 int main(void) { int m; // 先定义一个变量,使它自动获取地址 int *p = &m; *p = 2000; printf("*p = %d\n", *p); system("pause"); return 0; }
- 先定义一个变量,使它自动获取地址 (0-255 是预留给操作系统的)
- 报错:引发了异常,写入访问权限冲突
- 没有一个有效的地址空间的指针
- 出现野指针的情况
万能指针/泛型指针 (void *)
泛型指针,可以接收任意一种变量地址,但是在使用时必须借助强转具体化数据类型。
- 使用时必须强制转换,如果不强制转换,会报错:不允许使用不完整的类型
-
示例
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int a = 30; void *p; p = &a; printf("*p = %d\n", *(int *)p); system("pause"); return 0; }
const 关键字
- 作用
- 修饰变量
修饰变量不靠谱,仍然可以通过指针的方式修改变量。
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { const int a = 10; int* p = &a; *p = 30; printf("a = %d\n", a); system("pause"); return 0; }
- 修饰指针
const 向右修饰,被修饰的部分即为只读。常用的方式是在函数形参内,用来限制指针对应的内存空间为只读。
const int *p; // 可以修改 p,不可以修改 *p int const *p; // 可以修改 p,不可以修改 *p int * const p; // 可以修改 *p,不可以修改 p const int * const p; // *p 和 p 都不可以修改
- 修饰变量
- 使用场景
- 使用结构体作为函数参数
- 普通写法,传参是结构体变量
- 进阶写法,传参是结构体指针,节省资源
- 使用 const 修饰形参
- 未使用 const 修饰形参时,外部数据可以被更改
- const 修饰形参,防止误操作,修改时会报错
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include typedef struct person { char name[64]; int age; int id; double score; } p; // 1. 普通写法,传参是结构体变量 void showperson1(struct person p) { p.age = 66; // 可以修改外部数据 printf("name: %s, age: %d, id: %d, score: %.2lf\n", p.name, p.age, p.id, p.score); } // 2. 进阶写法,传参是结构体指针,节省资源 void showperson2(struct person* p) { p->age = 77; // 可以修改外部数据 printf("name: %s, age: %d, id: %d, score: %.2lf\n", p->name, p->age, p->id, p->score); } // 3. 使用 const 修饰形参,防止误操作 void showperson3(const struct person* p) { //p->age = 77; // Error! 不可以修改外部数据 printf("name: %s, age: %d, id: %d, score: %.2lf\n", p->name, p->age, p->id, p->score); } int main() { struct person p = { "Tom", 18, 10000, 50 }; showperson1(p); // name: Tom, age: 66, id: 10000, score: 50.00 showperson2(&p); // name: Tom, age: 77, id: 10000, score: 50.00 showperson3(&p); // name: Tom, age: 77, id: 10000, score: 50.00 system("pause"); return 0; }
- 使用结构体作为函数参数
间接访问 (解引用) 操作符 (*)
通过一个指针访问它所指向的地址的过程,叫做间接访问,或解引用,使用操作符 *。
- 指针声明时,* 表示所声明的变量为指针
-
指针使用时,* 表示操作指针所指向的内存空间
-
- 相当于通过地址找到指针指向的内存,再操作内存
-
- 放在等号左边,表示赋值 (给内存赋值,写内存)
-
- 放在等号右边,表示取值 (从内存中取值,读内存)
-
- 示例
#define _CRT_SECURE_NO_WARNINGS #include int main() { int* p = NULL; int a = 10; p = &a; *p = 20; // * 当左值,必须确保内存可写 int b = *p; printf("a = %d, *p = %d, b = %d\n", a, *p, b); // a = 20, *p = 20, b = 20 char* str = "hello"; //*str = 'm'; // !!error,必须确保内存可写 return 0; }
指针的步长
- 代表指针 +1 后跳跃的字节数
- char
- +1 = 1
- double
- +1= 8
- char
- 解引用,解出的字节数
#define _CRT_SECURE_NO_WARNINGS #include #include #include #include // offsetof() 使用 void test0401() { char buf[1024] = { 0 }; int a = 1000; memcpy(buf, &a, sizeof(int)); char* p = buf; printf("%d\n", *(int*)p); // 1000 printf("%d\n", *(int*)(p+1)); // 3 } void test0402() { char buf[1024] = { 0 }; int a = 1000; memcpy(buf+1, &a, sizeof(int)); char* p = buf; printf("%d\n", *(int*)(p + 1)); // 1000 }
- offsetof 宏函数
- 函数描述
- 通过宏函数找到属性对应的偏移量
- 函数原型
#include <>
- 示例
struct person { char a; // 0 ~ 3 int b; // 4 ~ 7 char buf[64]; // 8 ~ 71 int d; // 72 ~ 75 }; void test0403() { struct person p = { 'a', 10, "hello world", 20 }; printf("%p\n", &p); // printf("%p\n", &p+1); // 加了个结构体的总长 // 打印 d 的值 printf("%d\n", *((char*)&p + 72)); // 20 printf("%d\n", *(int *)((char *)&p + 72)); // 20 printf("%d\n", *(int *)((char *)&p + 72)); // 20 // offset 使用 printf("%d\n", offsetof(struct person, d)); // 72 printf("%d\n", *(int *)((char *)&p + offsetof(struct person, d))); // 20 }
- 函数描述
通过指针间接赋值
- 条件
- 一个普通变量 + 指针变量,或一个实参 + 一个形参
- 建立关系
- 通过 * 操作指针指向的内存
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include void test0501() { int a = 100; // 两个变量 int* p = NULL; p = &a; // 建立联系 *p = 22; // 通过 * 操作内存 printf("%d\n", a); // 22 } void test0502(int *str) // 一个实参,一个形参 { *str = 33; // 通过 * 操作内存 } int main(void) { test0501(); int a = 100; test0502(&a); // 传参的过程就是建立联系的过程,相当于 *str = &a printf("%d\n", a); // 33 system("pause"); return 0; }
- 用 n 级指针形参,去间接修改了 n-1 级指针 (实参) 的值
- Qt 每次分配的地址都相同,可以通过地址更改数据
指针和数组
数组名
- 数组名是一个地址常量 (不可以修改)
- 带有副作用的运算符 (++ — += -= /= %=)
- 指针是变量
- 可以用数组名给指针赋值
取数组元素
- 结论
int arr[] = {1, 3, 5}; int *p = arr; arr[0] == *(arr+0) == p[0] == *(p+0)
- 示例:验证以上结论
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int a[] = {1, 2, 3, 4, 5, 6}; int n = sizeof(a) / sizeof(a[0]); int* p = a; for (size_t i = 0; i < n; i++) { printf("%d ", a[i]); printf("%d ", p[i]); // 指针方式 printf("%d ", *(a+i)); printf("%d ", *(p + i)); putchar('\n'); } putchar('\n'); system("pause"); return 0; } /* 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 */
指针和数组区别
- 指针是变量,数组名为常量
- 大小
- sizeof(指针)
- 4 或 8 字节,与平台架构有关
- sizeof(数组)
- 数组的实际字节数
- sizeof(指针)
指针 ++ 操作数组
- 指针的 (* / %) 操作
- error!!!
- 指针加减运算
- 数据类型对指针的作用
- 间接引用
- 决定了从指针存储位置开始,向后读取的字节数 (与指针本身的存储空间无关)
- 加减运算
- 决定了指针进行 +1/-1 操作向后加过的字节数
- 示例:使用指针 ++ 操作数组元素
#define _CRT_SECURE_NO_WARNINGS #include #include // 间接引用 int test01(void) { int arr[] = { 1, 2, 3, 4, 5 }; int* p = arr; int n = sizeof(arr) / sizeof(arr[0]); for (size_t i = 0; i < n; i++) { printf("%d ", *p); p++; // 间接引用,向后偏移一个字节数 } return 0; } // 加减运算 int test02(void) { //int a = 0x2ff02; //int* p = a; // p+1 和 p 差 4 //short a = 0x2ff02; //short* p = a; // p+1 和 p 差 2 long long a = 0x2ff02; long long* p = a; // p+1 和 p 差 8 printf("p = %p\n", p); printf("p + 1 = %p\n", p + 1); return 0; }
- 间接引用
- 指针 +/- 整数
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { short a[] = { 1, 2, 3, 4, 5 }; printf("a = %p\n", a); printf("&a[0] = %p\n", &a[0]); printf("a + 1 = %p\n", a + 1); printf("&a = %p\n", &a); printf("&a + 1 = %p\n", &a + 1); system("pause"); return 0; }
- 普通指针变量 + – 整数
char *p; //打印 p、p+1,偏移 1 字节 short *p; //打印 p、p+1,偏移 2 字节 int *p; //打印 p、p+1,偏移 4 字节 long long *p; //打印 p、p+1,偏移 8 字节
- 在数组中 + – 整数
int arr[] = { 1, 2, 3, 4, 5 }; int* p = arr; p+3; // 向后偏移 3 个元素 p-2; // 向左偏移 2 个元素
- &数组名 + 1
- 加过一个数组的大小 = 数组元素个数 * sizeof(数据类型的字节数)
- 普通指针变量 + – 整数
- 指针 +/- 指针
“`c++
<h1>define _CRT_SECURE_NO_WARNINGS</h1>
<h1>include</h1>
<h1>include</h1>
<h1>include</h1>
int main(void)
{
int a[] = { 1, 2, 3, 4, 5 };
int* p = &a[2];
int* q = &a[5];<pre><code>//printf("p + q = %d", p + q); // 报错
printf("p – q = %d", p – q); // 结果为 3,是元素的偏移个数system("pause");
return 0;
</code></pre>}
“`
- 指针 + 指针
- error!!!
- 报错:表达式必须包含整型
- 指针 – 指针
- 对于普通变量来说
- 语法允许,但是无实际意义
- 对于数组来说
- 偏移过的元素个数
- 对于普通变量来说
- 指针 + 指针
- 示例:借助数组/指针实现 strlen 函数 (用于返回数组元素的个数)
#define _CRT_SECURE_NO_WARNINGS #include #include // 用数组实现 int mystrlen1(char str[]) { int i = 0; while (str[i]) { i++; } return i; } // 用指针实现 int mystrlen2(char str[]) { char* p = str; while (*p) { p++; } return p - str; } int main(void) { char a[] = "hello world"; int b = mystrlen1(a); int c = mystrlen2(a); printf("mystrlen1 = %d\n", b); printf("mystrlen2 = %d\n", c); system("pause"); return 0; }
- 数据类型对指针的作用
- 指针比较运算
- 对于普通变量来说
- 语法允许,但是无实际意义
- 对于数组来说
- 地址之间可以进行比较,得到的是元素存储的先后顺序
- 判断空指针
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int* p = NULL; if (p != NULL) { printf("p is not NULL.\n"); } else { printf("p is NULL!\n"); } system("pause"); return 0; }
- 对于普通变量来说
- 指针数组
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int a = 10; int b = 20; int c = 30; int* arr1[] = { a, b, c }; // 数组元素为 整型变量 地址 int* p1 = &a; int* p2 = &b; int* p3 = &c; int* arr2[] = { p1, p2, p3 }; // 数组元素为 数组 地址。整型指针数组,存的都是整型地址 printf("%d\n", *(arr2[0])); // == *(*(arr2+0)) == *(*arr2) == **arr2 system("pause"); return 0; }
- 一个存储地址的数组,数组内部所有元素都是地址
- 数组元素为整型变量地址
-
数组元素为数组地址
*(*(arr2+0)) == *(*arr2) == **arr2
- 指针数组的本质是一个二级指针
- 二维数组的本质也是一个二级指针
- 一个存储地址的数组,数组内部所有元素都是地址
- 多级指针
int a = 0; int *p = &a; // 一级指针是变量的地址 int **pp = &p; // 二级指针是一级指针的地址 int ***ppp = &pp; // 三级指针是二级指针的地址,三级指针就够用了 // int **p[]; // 相当于一个三级指针 int ****pppp = &ppp; // 四级指针是三级指针的地址
- 对应关系
- ppp &pp
- 三级指针
- *ppp pp &p
- 二级指针
- **ppp *pp p &a
- 一级指针
- ***ppp **pp *p a
- 普通整型变量
- 结论
- n 级指针是 n-1 级指针的地址
- ppp &pp
- 示例:验证以上对应关系
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { int a = 10; int* p = &a; int** pp = &p; // int **pp = &(&a) 不允许 int*** ppp = &pp; printf("*p = %d\n", *p); printf("**pp = %d\n", **pp); printf("***ppp = %d\n", ***ppp); printf("a = %d\n", a); system("pause"); return 0; } /* *p = 10 * *pp = 10 * **ppp = 10 a = 10 */
- 对应关系
指针和函数
栈帧
当函数调用时,系统会在 stack 上申请一块内存区域,用来供函数调用,主要存放形参和局部变量。当函数调用结束,这块内存区域被释放。
传值和传址
- 传值操作
- 函数调用期间,实参将自己的值,拷贝一份给形参
- 传址操作
- 函数调用期间,实参将地址值,拷贝一份给形参
- 地址值 -> 在 swap 函数栈帧内部,修改了 main 函数栈帧内部的局部变量值
函数参数
- 指针做函数参数
调用时,传有效的地址值。
int swap(int *a, int *b);
- 数组做函数参数
传递的不再是整个数组,而是数组的首地址 (一个指针),所以,当整型数组做函数参数时,我们通常在函数定义中,封装 2 个参数:
- 一个表示数组首地址
- 一个表示元素个数
// 以下传参形式等价 void bubblesort(int arr[10]) void bubblesort(int arr[]) void bubblesort(int *arr)
函数返回值
- 指针做函数返回值
指针做函数返回值,不能返回局部变量的地址值。
int *test_func(int a, int b);
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include int test_func(int a, int b) { return a + b; } int main(void) { int *c = test_func(10, 20); // 不能这样声明赋值 //printf("c = %d\n", c); printf("*c = %d\n", *c); // Error! system("pause"); return 0; }
- 示例
- 数组做函数返回值 (C 语言中不允许)
只能写成指针形式。
指针做函数参数的输入、输出特性
-
输入,在主调函数分配内存,被调函数使用
#define _CRT_SECURE_NO_WARNINGS #include #include #include void test0601(char *str) { strcpy(str, "hello world"); } void test0602(char* str) { printf("%s\n", str + 6); // world } int main(void) { char buf[1024]; // 分配在栈上 test0601(buf); // 主调函数分配内存,被调函数使用 printf("%s\n", buf); // hello world char* p = malloc(sizeof(char) * 64); // 分配在堆上 memset(p, 0, 64); // 清空内存 strcpy(p, "hello world"); test0602(p); if (p != NULL) { free(p); p = NULL; } system("pause"); return 0; }
- 输出,在被调函数中分配内存,在主调函数使用
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 输入特性 void test0601(char *str) { strcpy(str, "hello world"); } // 输入特性 void test0602(char* str) { printf("%s\n", str + 6); // world } void test0603(char **pp) { char* str = malloc(sizeof(char) * 64); memset(str, 0, 64); // 清空内存 strcpy(str, "hello world"); *pp = str; } void test0604() { char* p = NULL; test0603(&p); printf("%s\n", p); // hello world } int main(void) { char buf[1024]; // 分配在栈上 test0601(buf); // 主调函数分配内存,被调函数使用 printf("%s\n", buf); // hello world char* p = malloc(sizeof(char) * 64); // 分配在堆上 memset(p, 0, 64); // 清空内存 strcpy(p, "hello world"); test0602(p); if (p != NULL) { free(p); p = NULL; } test0604(); system("pause"); return 0; }
指针和字符串
指针和字符串
- 写法
- char str1[] = {‘h’, ‘e’, ‘l’, ‘\0’};
- 变量,可读可写
- char str2[] = “he”;
- 变量,可读可写
- char *str3 = “he”;
- 常量,只读
- str3 变量中,存储字符串常量 “he” 中首个字符 ‘h’ 的地址值
- str3[1] = ‘h’;
- 错误写法
- char *str4 = {‘h’, ‘e’, ‘\0’};
- 错误写法
- char str1[] = {‘h’, ‘e’, ‘l’, ‘\0’};
- 当字符串 (字符数组),做函数参数时
- 不需要提供2个参数,因为每个字符串都有 \0
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include int main(void) { char str1[] = "hello"; char* str2 = "hello"; printf("----------- 改前\n"); printf("str1[] = %p \n", str1); printf("str2 = %p\n", str2); printf("----------- 改后\n"); //str1[0] = 'R'; //str2[0] = 'R'; printf("str1[] = %p \n", str1); printf("str2 = %p\n", str2); system("pause"); return 0; }
字符串常用算法
- 常用函数
- strstr 函数
- 函数描述
- 查找在字符串 str 中,substr 子串出现的位置
- 函数原型
#include char *strstr(char *str, char *substr)
- 参数
- str
- 原字符串
- substr
- 子串
- str
- 返回值
- 查找成功,返回子串在原串中的位置 (地址值);如果没有,返回 NULL
- 参数
- 函数描述
- strstr 函数
- 字符串比较
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 数组实现 int mystrcmp1(char *str1, char *str2) { int i = 0; while (str1[i] == str2[i]) { if (str1[i] == '\0') { return 0; } i++; } return str1[i] > str2[i] ? 1 : -1; } // 指针实现 int mystrcmp2(char *str1, char *str2) { while (*str1 == *str2) { if ( *str1 == '\0') { return 0; } str1++; str2++; } return *str1 > *str2 ? 1 : -1; } int main(void) { char a[] = "hello"; char b[] = "hello"; int c = mystrcmp1(a, b); int d = mystrcmp2(a, b); printf("c = %d\n", c); printf("d = %d\n", d); system("pause"); return 0; }
- 比较 str1 和 str2,如果相同,返回 0;如果不同,依次比较 ASCII 码;如果 str1 > str2,返回 1,否则,返回 -1
- 利用数组/指针两种方法实现
- 字符串拷贝
- 将字符串拷贝到一个空数组中
#define _CRT_SECURE_NO_WARNINGS #include #include #include void mystrcpy1(char* src, char* dst) { int i = 0; while (src[i] != 0) { dst[i] = src[i]; i++; } dst[i] = '\0'; // 手动加个结束符 } void mystrcpy2(char* src, char* dst) { while (*src) { *dst = *src; src++; dst++; } } int main(void) { char a[] = "hello world"; char b[100] = { 0 }; char c[100] = { 0 }; printf("a: %s\n", a); mystrcpy1(a, b); mystrcpy2(a, c); printf("b: %s\n", b); printf("c: %s\n", c); system("pause"); return 0; }
- 拷贝过程中,字符串去空格
#define _CRT_SECURE_NO_WARNINGS #include #include #include void mystrcpy3(char* src, char* dst) { int i = 0; int j = 0; while (src[i] != 0) { if (src[i] != ' ') { dst[j] = src[i]; j++; } i++; } dst[i] = '\0'; // 手动加个结束符 } void mystrcpy4(char* src, char* dst) { while (*src) { if (*src != ' ') { *dst = *src; dst++; } src++; } } int main(void) { char a[] = "hello world"; char b[100] = { 0 }; char c[100] = { 0 }; printf("a: %s\n", a); mystrcpy3(a, b); mystrcpy4(a, c); printf("b: %s\n", b); printf("c: %s\n", c); system("pause"); return 0; }
- 将字符串拷贝到一个空数组中
- 在字符串中查找某个字符或子串
- 字符串 str 中子串 substr 出现的次数
#define _CRT_SECURE_NO_WARNINGS #include #include #include int str_times(char *str, char *substr) { int count = 0; char * p = strstr(str, substr); // llollollo -> llollo int substr_len = strlen(substr); // 每次重新查找都需要偏移的元素个数 while (p != NULL) { p += substr_len; p = strstr(p, substr); // 数组就是一个地址 p == 数组 count++; } return count; } int main(void) { char str[] = "llollollo"; char substr[] = "llo"; //int* p = strstr(str, substr); //printf("%p %p", p, str); int a = str_times(str, substr); printf("子串出现 %d 次.\n", a); system("pause"); return 0; }
- 在字符串中查找某个字符出现的位置
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 数组版本 char* mystrch1(char *str, char ch) // helloworld -> o { int i = 0; while (str[i]) { if (str[i] == ch) { return &str[i]; // 返回字符在字符串中的地址 //return i + 1; // 返回字符是字符串中的第几个元素 } i++; } } // 指针版本 char* mystrch2(char *str, char ch) { while (str) { if (*str == ch) { return str; } str++; } } int main(void) { char str[] = "hello world"; char ch = 'o'; int a = mystrch1(str, ch); int b = mystrch2(str, ch); printf("%s\n", a); printf("%s\n", b); system("pause"); return 0; }
- 求非空字符串的元素个数
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 数组版本 char* no_space_str1(char *str) // "hello world" -> 10 { int i = 0; int j = 0; while (str[i]) { if (str[i] != ' ') { j++; } i++; } return j; } // 指针版本 char* no_space_str2(char *str) { int j = 0; //char* p = str; while (*str) { if (*str != ' ') { j++; } str++; } return j; } int main(void) { char str[] = "hello world"; int a = no_space_str1(str); int b = no_space_str2(str); printf("%d\n", a); printf("%d\n", b); system("pause"); return 0; }
- 字符串 str 中子串 substr 出现的次数
- 头尾双指针
- 字符串逆置
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 指针版本 void str_reversed(char* str) // hello { char* start = str; // 首地址 char* end = str + strlen(str) - 1; // 末地址 char temp; while (start < end) { temp = *start; *start = *end; *end = temp; start++; end--; } } int main(void) { char str[] = "hello"; str_reversed(str); printf("%s\n", str); system("pause"); return 0; }
- 判断字符串是否是回文
“`c++
<h1>define _CRT_SECURE_NO_WARNINGS</h1>
<h1>include</h1>
<h1>include</h1>
<h1>include</h1>
// 指针版本
int str_palindrome(char <em>str)
{
char</em> start = str;
char* end = str + strlen(str) – 1;<pre><code>while (start < end)
{
if (*start == *end)
{
start++;
end–;
}
else
{
return -1;
}
}
return 1; // 循环完毕后,说明是回文,返回1
</code></pre>}
int main(void)
{
char str[] = "abcba";<pre><code>int a = str_palindrome(str);
printf("%s %s\n", str, a == 1 ? "是回文" : "不是回文");
system("pause");
return 0;
</code></pre>}
“`
- 字符串逆置
字符串指针强化
-
字符串指针做函数参数
- 八进制和十六进制的转义字符
八进制和十六进制的字符表示的是字符的 ASCII 码对应的数值。
- 八进制字符:’\ddd’,例如:’\063′ 表示字符 ‘3’,’3′ 的 ASCII 码是63(八进制)
- 十六进制字符:’\xhh’,例如:’\x41′ 表示字符 ‘A’
- sizeof 和 strlen 的区别
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { printf("%d\n", sizeof("hello world")); // 12 printf("%d\n", strlen("hello world")); // 11 system("pause"); return 0; }
- sizeof 计算字符串实际长度
- strlen 计算字符串中非空字符个数 (字符串结束标志是 ‘\0’)
- 示例
- 字符串拷贝
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 数组方式 void copystring1(char *dest, char *src) { int i = 0; int n = strlen(src); while (i <= n) // 如果这里是 i < n,后面就必须手动加上 \0 { dest[i] = src[i]; i++; } //dest[i] = '\0'; } // 指针方式 void copystring2(char* dest, char* src) { int n = strlen(src); for (int i = 0; i <= n; i++) { *dest = *src; dest++; src++; } } // 指针的进阶版 void copystring3(char* dest, char* src) { while(*dest++ = *src++) // 因为是 char 指针,所以可以++取出字符串 { } } int main(void) { char str[] = "hello world"; char dest[1024]; //int n = sizeof(str) / sizeof(str[0]); // 12 //printf("%d\n", n); //printf("%s\n", str); //copystring1(dest, str); //copystring2(dest, str); copystring3(dest, str); printf("%s\n", dest); system("pause"); return 0; }
- 字符串反转
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 数组方式 void copystring1(char *dest, char *src) { int i = 0; int n = strlen(src); while (i <= n) // 如果这里是 i < n,后面就必须手动加上 \0 { dest[i] = src[i]; i++; } //dest[i] = '\0'; } // 指针方式 void copystring2(char* dest, char* src) { int n = strlen(src); for (int i = 0; i <= n; i++) { *dest = *src; dest++; src++; } } // 指针的进阶版 void copystring3(char* dest, char* src) { while(*dest++ = *src++) // 因为是 char 指针,所以可以++取出字符串 { } } // 字符串反转-数组方式 void reversestring1(char* str) { int i = 0; int j = strlen(str) - 1; while (i < j) { char temp = str[i]; str[i] = str[j]; str[j] = temp; i++; j--; } } void reversestring2(char* str) { char* start = str; char* end = str + strlen(str) - 1; while (start < end) { char temp = *start; *start = *end; *end = temp; start++; end--; } } int main(void) { char str[] = "hello world"; char dest[1024]; //int n = sizeof(str) / sizeof(str[0]); // 12 //printf("%d\n", n); //printf("%s\n", str); //copystring1(dest, str); //copystring2(dest, str); //copystring3(dest, str); //printf("%s\n", dest); //reversestring1(str); reversestring2(str); printf("%s\n", str); system("pause"); return 0; }
- 字符串拷贝
- 八进制和十六进制的转义字符
- 字符串格式化函数
- sprintf 函数
- 函数原型
#include int sprintf(char *str, const char *format, ...);
- 参数
- str
- 字符串首地址
- format
- 字符串格式
- str
- 返回值
- 成功,返回字符串的有效长度,不包含 \0;失败,返回 -1
- 参数
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { char* str[1024]; //sprintf(str, "%s", "hello world"); sprintf(str, "%s %s", "hello", "world"); printf("%s\n", str); char str2[] = "hello\012world"; printf("%s\n", str2); printf("%d\n", sizeof(str2)); // 12 printf("%d\n", strlen(str2)); // 11 system("pause"); return 0; }
- 函数原型
- sscanf 函数
- 函数原型
#include int sscanf(const char *str, const char *format, ...);
- 参数
- str
- 指定的字符串首地址
- format
- 字符串格式,用法和 scanf() 一样
- str
- 返回值
- 成功,返回参数数目;失败,返回 -1
- 参数
- 示例
- 示例 1
// 提取 abab void match_abab() { char buf[] = "abc#abab@ccc1223"; char str[64] = { 0 }; sscanf(buf, "%*[a-c]#%[^@]", str); printf("str = %s\n", str); // abab } // 拆分 ip 地址 void match_ip() { char* ip = "192.16.0.30"; int num[4] = {0}; sscanf(ip, "%d.%d.%d.%d", &num[0], &num[1], &num[2], &num[3]); for (int i = 0; i < 4; i++) { printf("%d ",num[i]); // 192 16 0 30 } putchar('\n'); } // 提取 abb ccc.1223 void match_abb() { char buf[] = "abb@ccc.1223"; char str1[64] = { 0 }, str2[64] = { 0 }; sscanf(buf, "%[a-c]@%s", str1, str2); printf("%s %s\n", str1, str2); // abb ccc.1223 }
- 示例 2:查找字符串 “aaabcfegfa” 中 “abc” 出现的位置
#define _CRT_SECURE_NO_WARNINGS #include #include // str = "abcabd", substr = "cab",用 substr 和 str 中的每3个字符逐个比较 // 1. 数组方式 int strch1(char* str, char* substr) { int i = 0, j = 0; int strLen = strlen(str); int substrLen = strlen(substr); for (i = 0; i < strLen - substrLen; i++) { for (j = 0; j < substrLen; j++) { if (str[i + j] != substr[j]) // str[i] 偏移 j 个字符,和substr[j] 相等, { break; } } // 如果 substr 中每个字符都比较完了,说明查找到了位置 if (j == substrLen) { return i; } } return -1; } // 2. 指针方式 int strch2(char* str, char* substr) { int i = 0; int n = strlen(substr); while (*str) { int j = 0; char* tmpstr = str; char* tmpsubstr = substr; while (*tmpsubstr) { if (*tmpstr == *tmpsubstr) // 解引用的方式逐个比较每个字符 { tmpstr++; tmpsubstr++; j++; continue; } else { break; } } if (j == n) { return i; } else { str++; i++; } } return -1; } int main(void) { char* str = "aaabcfegfa"; char* substr = "abc"; int a = strch1(str, substr); int b = strch2(str, substr); printf("substr 的位置是:a = %d, b = %d\n", a, b); // substr 的位置是:a = 2, b = 2 printf("%d\n", strcmp("abc", "abc")); // 相等返回 0 system("pause"); return 0; }
- 示例 1
- 函数原型
- 常用格式
- 进制
- %o
- 八进制,0 前缀用于区分八进制
- %x
- 十六进制,0x 前缀用于区分十六进制
- 示例
sprintf(buf, "0%o", 100) sprintf(buf, "0x%x", 100)
- %o
- 正则匹配
- %s 或 %d
*
表示跳过 (忽略) 数据,遇到空格或 \t,忽略结束- %8d
- 左对齐
- %-8d
- 右对齐
- %[width]s
- 读指定宽度的数据
- %[a-z]
- 匹配 a 到 z 任意字符
- 贪婪匹配 (尽可能多的匹配),只要匹配失败,就不会再继续匹配
- %[aBc]
- 匹配 a、B、c 中一员,贪婪匹配
- %[^a]
- 匹配非 a 的任意字符,贪婪性
- %[^a-z]
- 读取除 a-z 以外的所有字符
- 示例
// 1. 忽略数字 or 字符 void ignore_digit_or_characters() { char buf[] = "123abc"; int num = 0; char str[64] = {0}; sscanf(buf, "%d%*s", &num); sscanf(buf, "%*d%s", str); //sscanf(buf, "%d%s", &num, str); printf("num = %d, str = %s\n", num, str); // num = 123, str = abc } // 2. 匹配指定宽度的数据 void getDataWidth() { char buf[] = "123abc"; char str[64] = {0}; sscanf(buf, "%4s", str); printf("str = %s\n", str); // str = 123a } // 3. 匹配 a-b 中任意字符,贪婪匹配 void match_a_to_b() { char buf[] = "123abc"; char str[64] = { 0 }; sscanf(buf, "%*d%[a-b]", str); printf("str = %s\n", str); // str = ab sscanf(buf, "%*d%[a-c]", str); printf("str = %s\n", str); // str = abc } // 4. 匹配 a B c 中的其中一个,只要匹配失败,就不继续匹配了 void match_aBc() { char buf[] = "aBaBcaBd"; char str[64] = { 0 }; sscanf(buf, "%[aBc]", str); printf("str = %s\n", str); // str = aBaBcaB } // 5. 匹配非 a 的字符 void match_nota() { char buf[] = "123BaBbabBc"; char str[64] = { 0 }; sscanf(buf, "%[^a]", str); printf("str = %s\n", str); // str = 123B }
- %s 或 %d
- 进制
- sprintf 函数
带参数的 main 函数
- 函数原型
int main(void); //== int main() int main(int argc, char *argv[]); // == int main(int argc, char **argv)
- argc
- 表示给 main 函数传递的参数的总个数
- argv[]
- 是一个存放参数的数组,数组的每个元素都是 char * (char * 就是字符串)
- argc
- 测试
- 命令行终端中编译,生成可执行文件
test.exe abc asewr werq #算上 .exe 文件,共4个参数,argc = 4
- VS 中传入命令参数
- VS 项目属性 – 调试 – 命令参数
- VS 项目属性 – 调试 – 命令参数
- 循环打印外部参数
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(int argc, char *argv[]) { //printf("argc = %d\n", argc); for (size_t i = 0; i < argc; i++) { printf("argv[%d]=%s\n", i, argv[i]); //printf("%s\n", argv[i]); } system("pause"); return 0; }
- 命令行终端中编译,生成可执行文件
一级指针易错点
越界
// 越界
void test01()
{
char buf[3] = "abc"; // 应该是 buf[4],缺少 \0
printf("%s\n", buf); // 显示乱码:abc烫烫虄?O
}
- 字符串长度定义错误,缺少最后的 ‘\0’,使用 printf 打印时因为找不到 ‘\0’,所以会输出乱码
指针叠加会不断改变指针方向,在释放内存时要注意
// 指针叠加会不断改变指针方向
void test02()
{
char* p = (char*)malloc(50);
char* tp = p; // 通过创建临时指针来操作内存,防止出错
char buf[] = "abcdef";
int n = strlen(buf);
for (int i = 0; i < n; i++)
{
*tp = buf[i];
printf("%c %c\n", buf[i], *tp); // buf[i] == *tp
tp++; // 更改指针位置,直接释放原指针会出错
}
free(p);
p = NULL;
}
- 报错
- 解决方法:使用临时变量保存初始指针
同一块内存释放多次
// 同一块内存释放多次
void test03()
{
char* p = (char*)malloc(50);
strcpy(p, "hello");
printf("%s\n", p); // hello
if (p != NULL)
{
// 只是告诉系统,p 指向的内存可以回收了,这块内存的使用权交还给系统;
// 但是,p 的值还是原来的值 (野指针),p 还是指向原来的内存
free(p);
}
// Error! 不能再次释放
//if (p != NULL)
//{
// free(p);
//}
}
- 不可以释放野指针 (已被回收的指针)
返回局部变量地址
// 返回一个局部变量
char* getstr()
{
char str[] = "afasdfasf"; // 在栈上开辟内存
printf("%s\n", str); // afasdfasf
return str; // Error!
}
int main()
{
printf("%s\n", getstr()); // 烫烫烫烫烫烫@
system("pause");
return 0;
}
- 局部变量创建在栈上,在函数调用结束后被释放
多级指针
二级指针做函数参数的输入、输出特性
- 输入特性
- 在主调函数分配内存,被调函数使用
- 创建在栈上/堆上
- 输出特性
- 被调函数分配内存,主调函数使用
- 同级指针释放,要手动释放一次
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include // 打印数组 void printArr1(int** arr, int n) { for (size_t i = 0; i < n; i++) { printf("%d ", *arr[i]); // 1 2 3 } putchar('\n'); } // 输入特性:主调函数分配内存,被调函数使用 void test01() { // 在栈上创建数据 int a = 1, b = 2, c = 3; // 开辟堆区内存 int** arr = malloc(sizeof(int*) * 3); arr[0] = &a; arr[1] = &b; arr[2] = &c; // 打印数组 printArr1(arr, 3); // 释放堆区内存 free(arr); arr = NULL; } // 输出特性:被调函数分配内存,主调函数使用 void allocateSpace(int** tp) { int* p = malloc(sizeof(int) * 3); for (size_t i = 0; i < 3; i++) { p[i] = i + 10; printf("%d ", p[i]); // 10 11 12 } putchar('\n'); *tp = p; } // 打印数组 void printArr2(int** arr, int n) { for (size_t i = 0; i < n; i++) { printf("%d ", (*arr)[i]); // 10 11 12 要先做解引用 } putchar('\n'); } int main() { test01(); int* p = NULL; allocateSpace(&p); printArr2(&p, 3); // 手动释放堆区内存 if (p != NULL) { free(p); p = NULL; printf("此时为空指针"); } system("pause"); return 0; }
使用二级指针进行文件读写
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 打开文件,输出文件行数 int getRow(FILE* fp) { // 判断文件是否打开成功 if (fp == NULL) { perror("fopen error"); return -1; } int i = 0; char buf[10] = { 0 }; while (fgets(buf, 10, fp) != NULL) { //printf("%s", buf); i++; } //putchar('\n'); fseek(fp, 0, SEEK_SET); // 将文件光标恢复到开头位置 return i; } // 读取文件到 pArray void fileRead(FILE* fp, int n, char** pArray) { // 判断参数合法性 if (!fp || (n == 0) || (pArray == NULL)) { perror("fopen error"); return; } int i = 0; char buf[1024] = { 0 }; while (fgets(buf, 1024, fp) != NULL) { int len = strlen(buf) + 1; //printf("%d\n", len); char* currentStr = malloc(sizeof(char) * len); strcpy(currentStr, buf); //printf("%s", currentStr); pArray[i++] = currentStr; memset(buf, 0, 1024); // 清理内存 } } // 打印 pArray 中的数据 void printData(char** pArray, int n) { for (size_t i = 0; i < n; i++) { printf("%s", pArray[i]); } putchar('\n'); } int main(void) { // 打开文件 FILE* fp = fopen("./test.txt", "r"); // 输出文件行数 int n = getRow(fp); printf("%d\n", n); // 4 // 创建堆区 char** pArray = malloc(sizeof(char*) * n); // 读取文件到 pArray fileRead(fp, n, pArray); // 打印 pArray 中的数据 printData(pArray, n); // 文件中的中文内容显示为乱码 // 释放堆区 free(pArray); pArray = NULL; // 关闭文件 fclose(fp); system("pause"); return EXIT_SUCCESS; }
- 遗留问题:fopen 的文件内容中,中文显示为乱码,是因为本电脑的系统编码是 GBK 格式,而读取的文件编码是 UTF-8
多维数组
一维数组名
-
除了两种特殊情况,都是指向数组首元素的指针
- sizeof 统计的是数组长度
- 对数组名取地址,得到的数组指针的步长是整个数组长度
- 数组名是指针常量,指针的方向不可以修改,但是指针指向的值可以修改
-
可读性
- 传参时,int arr[] 可读性更高
- 数组索引下标可以为负数,例如:p[-1] 表示最后一个元素
-
示例
void test01() { int arr[5] = { 1, 2, 3, 4, 5 }; printf("%d\n", sizeof(arr)); // 20 printf("%d\n", &arr); // 17823928 printf("%d\n", &arr + 1); // 17823948 步长是整个数组长度 20 //arr = NULL; // Error! 指针的指向不可以修改 arr[0] = 9; // 但是指向的值可以修改 printf("%d\n", arr[0]); // 9 int* p = arr; p = p + 3; printf("%d\n", p[-1]); // 3, p = p -1,索引可以为负数, p[-1] 是最后一个元素 printf("%d\n", *(p - 1)); // 3 }
数组指针的定义方式
- 先定义数组类型,再通过类型定义数组指针变量
-
先定义数组指针类型,再通过类型定义数组指针变量
-
直接定义数组指针变量
-
示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 1. 先定义数组类型,再通过类型定义数组指针变量 void test01() { int arr[5] = { 1, 2, 3, 4, 5 }; typedef int(ARRAY_TYPE)[5]; // ARRAY_TYPE 代表存放 5 个 int 类型元素的数组 的数组类型 ARRAY_TYPE array = {0}; for (int i = 0; i < 5; i++) { array[i] = i + 10; printf("%d\n", array[i]); // 10 11 12 13 14 } ARRAY_TYPE* arrP = &arr; // *arrP == arr == 数组名 for (int i = 0; i < 5; i++) { printf("%d\n", (*arrP)[i]); // 1 2 3 4 5 } } // 2. 先定义数组指针类型,再通过类型定义数组指针变量 void test02() { int arr[5] = { 1, 2, 3, 4, 5 }; typedef int(*ARRAY_TYPE)[5]; // 先定义一个指针类型 ARRAY_TYPE arrP = &arr; for (int i = 0; i < 5; i++) { printf("%d\n", (*arrP)[i]); // 1 2 3 4 5 } } // 3. 直接定义数组指针变量 void test03() { int arr[5] = { 1, 2, 3, 4, 5 }; int(*p)[5] = &arr; // 定义一个数组指针变量 for (int i = 0; i < 5; i++) { printf("%d\n", (*p)[i]); // 1 2 3 4 5 } } int main(void) { test01(); printf("-----------------\n"); test02(); printf("-----------------\n"); test03(); system("pause"); return 0; }
二维数组名
- 定义方式
- 直接指定行列
- 自动分配行列
- 二维数组名称是指向第一个一维数组的数组指针 (与一维数组名情况相同)
- 特殊情况
- sizeof 统计二维数组大小
-
对数组名取地址
int (*p)[3][3] = &arr
- 特殊情况
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include void test01() { // 1. 直接指定行列 int arr1[2][3] = { {1, 2, 3}, {4, 5, 6} // 这行的逗号可有可无 }; // 2. 自动分配行列 int arr2[2][3] = { 1, 2, 3, 4, 5, 6 }; int arr3[][3] = { 1, 2, 3, 4, 5, 6 }; // 至少要指定列 printf("%d\n", sizeof(arr1)); // 24 = 2 * 3 * 4 printf("arr1 地址: %p\n", &arr1); // arr1 地址: 000000DDBE74F6E8 printf("arr1 地址偏移1: %p\n", &arr1 + 1); // arr1 地址偏移1: 000000DDBE74F700,和上面差24 int(*pArray)[3] = arr1; // 定义数组指针 printf("%d\n", arr1[1][2]); // 6,具有可读性,给人看的 printf("%d\n", *(*(pArray + 1) + 2)); // 6,给机器看的 } int main(void) { test01(); system("pause"); return EXIT_SUCCESS; }
二维数组做函数参数
- 数组做函数参数
void printArray1(int arr[][3], int row, int col) void printArray1(int arr[2][3], int row, int col)
- 数组指针做函数参数
void printArray2(int (*array)[3], int row, int col)
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 1. 数组做函数参数 //void printArray1(int arr[2][3], int row, int col) void printArray1(int arr[][3], int row, int col) { for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { printf("%d ", arr[i][j]); } putchar('\n'); } } // 2. 数组指针做函数参数 void printArray2(int(*array)[3], int row, int col) { for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { printf("%d ", *(*(array + i) + j)); } putchar('\n'); } } int main() { int arr1[2][3] = { {1, 2, 3}, {4, 5, 6} // 这行的逗号可有可无 }; printArray1(arr1, 2, 3); printArray2(arr1, 2, 3); system("pause"); return 0; }
指针数组
- 数组指针和指针数组的区别
- 数组指针
- 指向数组的指针
- 指针数组
- 由指针组成的数组
- 数组指针
- 指针数组排序
- 指针数组排序
- 不能用地址做对比,要用 strcmp 函数
- 示例:选择排序 (效率高于冒泡排序)
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 选择排序 -> 从小到大 //void selectSort(int* str[], int len) void selectSort1(int* str, int len) { for (int i = 0; i < len; i++) { int min = i; // 记录最小值的下标为 i for (int j = i + 1; j str[j]) { min = j; // 更新真实最小值下标 } } if (min != i) { int temp = str[i]; str[i] = str[min]; str[min] = temp; } //printf("%d\n", str[i]); } } // 打印数组 void printStr1(int* str, int len) { for (int i = 0; i 从大到小 void selectSort2(char* str[], int len) { for (int i = 0; i < len; i++) { int max = i; for (int j = i + 1; j < len; j++) { //if (str[i] 指针方式 void printStr2(char* str[], int len) { for (int i = 0; i < len; i++, str++) { printf("%s ", *str); } putchar('\n'); } int main(void) { int str1[] = { 1, 5, 3, 4 }; int len1 = sizeof(str1) / sizeof(str1[0]); printf("len1 = %d\n", len1); // 4 selectSort1(str1, len1); printStr1(str1, len1); // 1 3 4 5 char *str2[] = { "eee", "bbb", "ddd", "ccc" }; int len2 = sizeof(str2) / sizeof(str2[0]); printf("len2 = %d\n", len2); selectSort2(str2, len2); printStr2(str2, len2); system("pause"); return EXIT_SUCCESS; }
- 指针数组排序