C语言:第15天笔记
内容提要
- 指针
- 数组指针
- 指针数组
- 字符数组和字符指针
- 指针函数
回顾
c
#include <stdio.h>
/**
* 指向一维数组的指针
*/
int t_p1()
{
// 创建一个一维数组
int arr[] = {10,20,30,40,50};
// 计算数组大小
int len = sizeof(arr) / sizeof(arr[0]);
// 创建一个数组指针指向一维数组arr
int (*p)[len] = &arr;
// 借助数组指针遍历数组
for (int i = 0; i < len; i++)
{
// p 指向 arr这个数组,p存储了arr这个数组的地址
// 如果通过指针访问数组:*p *和[]在一起,[]的优先级大于*
printf("%-4d", (*p)[i]);
}
printf("\n");
}
/**
* 指向二维数组的指针
*/
int t_p2()
{
// 创建一个二维数组
int arr[][3] = {
{10,20,30},
{100,200,300},
{1000,2000,3000}
};
// 获取行和列的容量
int row_len = sizeof(arr) / sizeof(arr[0]);
int col_len = sizeof(arr[0]) / sizeof(arr[0][0]);
// 方式1,二维数组指针指向二维数组 不推荐
int (*p)[][3] = &arr;
// 遍历数组
for (int i = 0; i < row_len; i++)
{
for (int j = 0; j < col_len; j++)
{
printf("%-6d", (*p)[i][j]);
}
}
printf("\n");
// 方式2,一维数组指针指向二维数组,本质上是一体维数组指针指向二维数组的行(默认首行)推荐
// &arr:获取该二维数组的地址,范围作用于整个数组
// arr:数组名默认指向第一个元素,这里就是行,默认首行,范围作用于整个行,等价于 &arr[0]
// 数组参与指针运算,会降级为指针
int (*p1)[3] = arr;
for (int i = 0; i < row_len; i++)
{
for (int j = 0; j < col_len; j++)
{
printf("%-6d",p1[i][j]);
printf("%-6d",(*(p1+i))[j]);
printf("%-6d",*(p1[i]+j));// 列偏移
printf("%-6d",*(*(p1+i)+j));
}
}
printf("\n");
}
int main(int argc,char *argv[])
{
t_p1();
t_p2();
}
指针
数组指针与指针数组
数组指针
指针和数组中符号优先级
c
()> [] > *
通过指针引用二维数组
表示形式 | 含义 | 地址/值 |
---|---|---|
arr |
二维数组名,指向一维数组arr[0],0行首地址 | 2000 |
arr[0],*(arr+0),*arr |
0行0列元素地址,数组降级为指针 | 2000 |
arr + 1 , &arr[1] |
1行首地址 | 2008 |
arr[1],*(arr + 1) |
1行0列元素arr[1][0] 的地址 |
2008 |
arr[1]+2,*(arr+1)+2,&arr[1][2] |
1行2列元素arr[1][2] 的地址 |
2012 |
*(arr[1]+2),*(*(arr+1)+2),arr[1][2] |
1行2列元素arr[1][2] 的值 |
元素值为13 |
注意:二维数组中,数组整体的地址值 == 数组中0行元素的地址值 == 数组中0行0列元素的地址值
案例
案例1
-
需求:用指向元素的指针变量输出二维数组元素的值
-
代码:
c#include <stdio.h> int main(int argc,char *argv[]) { // 定义一个二维数组 int arr[3][4] = {10,20,30,40,100,200,300,400,1000,2000,3000,4000}; // 定义一个指针变量,用来指向数组中的元素 int *p = /* *(arr+0) |*/ *arr /* | arr[0] */; // 获取元素个数 = 行容量 * 列容量 int len = (sizeof(arr)/sizeof(arr[0])) * (sizeof(arr[0])/sizeof(arr[0][0])); // 使用单层for循环遍历二维数组,列 for (; p < arr[0] + len; p++) { // 每四个一行 p1 - p2 = (p1地址 - p2地址) / sizeof(type) if ((p - arr[0]) % 4 == 0 && p != arr[0]) printf("\n"); printf("%-6d", *p); } printf("\n"); return 0; }
案例2:
-
需求:数组指针- 输出二维数组任意行任意列的元素的值
-
代码:
c#include <stdio.h> int main(int argc,char *argv[]) { // 定义一个二维数组 int arr[3][4] = {1,3,5,7,11,33,55,77,111,333,555,777}; // 创建一个数组指针指向二维数组 int (*p)[4] = arr; // 二维数组中的第一个元素就是首行 int (*p)[3][4] // 创建两个变量,接收控制台输入的行和列 int row, col; printf("请输入行号和列号:\n"); scanf("%d,%d",&row,&col); printf("arr[%d][%d]=%d,%d,%d,%d\n",row,col,*(*(p+row)+col),(*(p+row))[col],*(p[row]+col),p[row][col]); return 0; }
指针数组
**定义:**指针数组是一个数组,数组中每一个元素都是一个指针。
特点:
- 先有指针,后有数组
- 指针数组的本质是一个数组,只是数组中的每一个元素是指针。
语法:
c
数据类型 *数组名[容量];
案例:
c
> Description:
#include <stdio.h>
int main(int argc,char *argv[])
{
// 定义三个变量
int a = 10, b = 20, c = 30;
// 定义指针数组:先有指针,后有数组
int *arr[3] = {&a, &b, &c};
// 获取大小
int len = sizeof(arr) / sizeof(arr[0]);
// 遍历数组
for (int i = 0; i < len; i++)
{
printf("%-3d", *arr[i]);
}
printf("\n");
return 0;
}
建议:我们一般使用指针数组处理字符串,后续专门讲解。
数组指针与指针数组的区别
对比项 | 指针数组 | 数组指针 |
---|---|---|
定义 | 数组元素均为指针的数组 | 指向一个完整数组的指针 |
存储内容 | 存储多个指针,每个元素指向不同内存地址 | 存储单个指针,指向一个完整的数组(首地址) |
内存分配 | 每个指针元素独立分配内存,可能分散 | 指向的数组内存连续,指针本身存储数组首地址 |
语法示例 | int *arr[5] (元素为5个int*指针) |
int (*arr)[5] (指向5个int的数组的指针) |
访问方式 | 通过下标访问指针元素,再解引用: *arr[i] |
先解引用指针得到数组,再访问元素: (*arr)[i] |
使用场景 | 管理多个独立指针(如字符串数组、动态结构体数组) | 操作多维数组(如传递二维数组的行指针) |
内存布局 | [ptr1] → 数据1 [ptr2] → 数据2 ... |
ptr → [数据1][数据2]... |
示例代码 | int a=1, b=2;int *arr[] = {&a, &b}; |
int arr[2][3] = {1,2,3,4,5,6};int (*ptr)[3] = arr; |
字符数组与字符指针
字符串实现
在C语言中,表示一个字符串有以下两种方式:
① 数组形式:用字符数组存放一个字符串
② 指针形式:用字符指针指向一个字符串
案例
c
#include <stdio.h>
/**
* 方式1:使用字符数组实现字符串
*/
void str_test1()
{
// 定义一个伪字符串
char str[] = "I LOVE YOU";
// 数组名是一个常量,也就是不支持赋值
// str = "YUE QIAN"; // 编译报错,常量不支持修改,替代方案:strcpy(str,"YUE QIAN");
printf("%s\n", str); // 数组在传参时,会被降级为指针
}
/**
* 方式2:使用字符指针指向字符串
*/
void str_test2()
{
// 定义一个伪字符串
char *str = "I LOVE YOU"; // 指针str指向一个字符串常量
// 改变str的指向
// str = "YUE QIAN";
printf("%s\n", str);
}
int main(int argc,char *argv[])
{
str_test1();
str_test2();
char arr[200];
char *str = arr; // 局部变量,如果未初始化,默认是随机值,如果是指针变量,地址值就是随机
printf("请输入一个字符串:\n");
scanf("%s", str);
printf("%s\n", str);
return 0;
}
注意:字符数组和字符指针变量都能实现字符串的存储与运算。(字符指针---> 字符类型的指针变量)
字符数组和字符指针的联系
概念
-
字符数组由元素组成,每个元素中存放一个字符;而字符指针(指向char类型的指针变量)中存放的是地址;
-
只能对字符数组中的各个元素赋值,而不能用赋值语句对整个字符数组赋值。
cchar arr[3] = {}; // 等价于 {'\0'}; 等价于 {0}; arr[2] = 'A'; // 正确,对字符数组的元素赋值 arr = {'E','D','F'};// 错误,数组名是常量,不能对其进行整体赋值
-
字符数组名虽然代表地址,但是数组名的值不能改变,因为数组名是常量。
cchar a = 'A'; char arr[50] = {}; char *p = arr; // 指针p指向数组第一个元素,p存储的是arr数组中第一个元素的地址 p = &a; // 改变指针p的指向,使其指向变量a,p存储的是a的地址 arr = &a; // 错误:数组名虽然是指针,但是数组名同时也是常量,所以不能赋值 p++; // p指向arr的第2个元素 arr+5; *p = 'W'; printf("%c %c\n", *p, *arr); return 0; }
-
对于字符串中字符的存取,可以用下标法,也可以用指针法。
c#include <stdio.h> int main(int argc,char *argv[]) { // 使用两种方式创建字符串 char str1[] = "你好,双哥哥!"; char *str2 = "你好,豪哥哥!";// 0x11 // 赋值测试 // str1 = "你好,强哥哥!"; // 错误,数组一旦创建,就无法改变其值。 str2 = "你好,帅哥哥!";// 0x12 // 打印测试 printf("%s,%s\n", str1, str2); // 测试从控制台获取一个字符串 // char *str;// 此时默认值是NULL,NULL对应的空间地址0x00000000,这块空间拒绝访问 // printf("请输入一个字符串:\n"); // scanf("%s",str); // printf("输出-%s\n",str);// 输出NULL // 注意:从控制台接收一个字符串只能用字符数组 char a[] = "I LOVE YOU!"; char *b = "I LOVE YOU!"; printf("%c,%c\n%c,%c\n%s,%s\n",a[2],*(a+2),b[2],*(b+2),a+2,b+2);// L,L L,L 1 2 return 0; }
字符串作为形参
定义
-
实参与形参都可以是字符数组
cvoid fun(char str[], int len) {..} void main() { char str[] = "hello"; int len = sizeof(str) / sizeof(str[0]); fun(str, len) }
-
实参用字符数组,形参用字符指针
cvoid fun(char *str, int len) {..} void main() { char str[] = "hello"; int len = sizeof(str) / sizeof(str[0]); fun(str, len) }
-
形参和实参都是字符指针。(在函数内部不能对字符串常量中的字符做修改)
c#include <stdio.h> void fun(char *str, int len) { printf("%s\n", str);// 0x1000 str依然指向"hello"这个常量空间 // *(str+1) = 'E';// 编译错误 不能修改"hello"这个常量空间的数据 // str[2] = 'L'; // 编译错误 str = "zhangsanfeng"; // 0x2000 此时并没有改变常量空间的数据,只是改变了指针的指向 printf("%s\n", str); } void main() { char *str = "hello"; // 0x1000 str指向的"hello"是一个常量空间,常量空间不支持修改 int len = sizeof(str) / sizeof(str[0]); fun(str, len); // 0x1000 }
-
实参是字符指针,形参是字符数组。(在函数内部不能对字符串常量中的字符做修改)
c#include <stdio.h> void fun(char str[], int len) { printf("%s\n", str);// 0x1000 str依然指向"hello"这个常量空间 // *(str+1) = 'E';// 编译错误 不能修改"hello"这个常量空间的数据 // str[2] = 'L'; // 编译错误 str = "zhangsanfeng"; // 此时并没有改变常量空间的数据,只是改变了指针的指向 printf("%s\n", str); } void main() { // char str[] = {'h','e','l','l','o','\0'}; // 这个可以看做是字符串变量,这个是支持修改元素 char *str = "hello"; // 0x1000 str指向的"hello"是一个常量空间,常量空间不支持修改 int len = sizeof(str) / sizeof(str[0]); fun(str, len);// 0x1000 }
注意
-
字符数组在创建的时候,会在内存中开辟内存空间,内存空间可以存放字符数据;字符指针在创建的时候,需要依赖于字符数组,字符指针在内存开辟的内存空间中,存放的是数组元素的地址。字符指针的创建依赖于字符数组,字符数组可以独立存在,而字符指针不能独立存在。
-
字符数组可以初始化,但是不能赋值;字符指针可以初始化,也可以赋值。
cchar str1[] = "hello"; // 对数组初始化 str1 = "hi"; // 对数组赋值,此时错误 str1[0] = 'H'; // 对数组中的元素赋值,此时正确
案例
案例1
-
字符指针作为函数参数:用函数调用实现字符串的复制以及长度计算
-
代码:
c#include <stdio.h> /** * 定义一个函数,实现字符串拷贝 * @param source 拷贝的源字符串,该字符串不能被修改 * @param dest 需要拷贝的目标数组 * @return 字符串的长度 */ int _str_cpy(const char *source, char *dest) { // 定义一个循环变量 register int i = 0; // 遍历循环 while (source[i] != '\0') { // 实现拷贝 *(dest + i) = *(source + i); // 等价于 dest[i] = source[i]; i++; } // 拷贝结束,一定要给dest中插入\0 *(dest + i) = '\0'; return i; } int main(int argc,char *argv[]) { char source[20],dest[20]; printf("请输入一个字符串:\n"); scanf("%s", source); int size = _str_cpy(source, dest); printf("字符串:%s的长度是%d\n", dest, size); return 0; }
案例2
-
需求:字符指针作为函数的参数-给定一个字符串,截取start到end之间的字符串,含头不含尾
-
代码:
c#include <stdio.h> /** * 定义一个函数,实现字符串的截取 * @param source 源字符串(字符数组、字符串常量、字符指针) * @param start 开始位置 * @param end 结束位置 * @param dest 目标数组(字符数组) * @return 目标字符串长度 */ int str_substr(const char *source, int start, int end, char *dest) { register int i = 0, k = 0; // 遍历字符串 while (source[i] != '\0') { // 根据start和end截取 if (i >= start && i < end) // 含头不含尾 { *(dest + k) = *(source + i); // hello k++; } i++; } *(dest + k) = '\0'; return k; } int main(int argc,char *argv[]) { char *str = "abcdefg";// cde char dest[26]; int len = str_substr(str,2,5,dest); printf("%s,%s,%d\n", str, dest, len);// abcdefg,cde,3 return 0; }
函数指针与指针函数
指针函数
定义:
本质上是上函数,这个函数的返回值类型是指针,这个函数称之为指针函数。(返回值是指针的函数叫做指针函数)
语法:
c
// 写法1
返回类型* 函数名(形参列表)
{
函数体;
return 指针;
}
// 写法2
返回类型 *函数名(形参列表)
{
函数体;
return 指针;
}
举例:
c
int *get(int a)
{
int *p = &a;
return p;
}
int main()
{
int *a = get(5);
printf("%d\n", *a);
}
注意:
在函数中不要直接返回一个局部变量的地址。因为函数调用完毕后,随着栈帧的回收,变量空间会销毁,使得返回的地址就不明确,此时返回的指针叫做野指针。
解决方案:
如果非要访问,可以给这个局部变量添加(定义的时候添加)static
,可以延长它的生命周期,从而避免野指针(尽量少用,因为存在内存泄漏)
演示案例:
c
#include <stdio.h>
int *add(int a, int b)
{
static int sum;
sum = a + b;
return ∑// 执行完return 作为函数作用域的布局变量sum的空间被释放
}
int main(int argc,char *argv[])
{
int *res = add(5,3); // 接收到了地址,但是地址对应的空间已经释放
printf("%d\n", *res);
return 0;
}
案例
-
需求:有若干个学生,每个学生有4门成绩,要求在用户输入学号(int id)后,能输出该学生的全部成绩(float scores[4]),用指针函数实现。
-
代码:
c#include <stdio.h> /** * 定义一个函数,要求输入学号,返回该学号对应学生的4门成绩 * @param all:所有人的成绩传进来 查找源头 * @param id:要检索学生的学号 * @return id对应学生的4门成绩 */ float* search(float (*all)[4], int id) { // 定义一个指针变量,用来接收查询到的学生的所有成绩 float *pt; pt = *(all + id);// 行偏移 {10,20,30,40} return pt; } int main(int argc,char *argv[]) { // 准备一个二维数组,存储3个学生的成绩 float scores[3][4] = { {60,70,80,90}, // 0x2000 0x2004 0x2008 0x200C {66,77,88,99}, // 0x2010 0x2014 0x2018 0x201C {61,71,81,91} }; // 定义一个变量,用来接收学生学号 int id; printf("请输入学生学号(0~2):\n"); scanf("%d", &id); printf("第%d个学生的成绩:\n", id); // 创建一个指针,用来接收成绩 float *p;// 0x11 --> 0x2000 p = search(scores, id);// 0x21 --> 0x2000 // 遍历 for (; p < scores[id] + 4; p++) { printf("%5.2f\t", *p); } // *(p + i) printf("\n"); return 0; }