C语言(三)
8.指针
8.1引入
一个变量有几种含义
- 空间
- 值
当我们顶一个一个变量之后,这个变量就可以代表这个空间本身,也可以表示空间中的值
在内存中,为了方便对内存的管理,计算机给每个字节都进行了编址。
对于整型数据,我们可以使用 int
、short
等类型进行保存,对于字符我们可以用 char
类型进行保存,但是我们要保存地址呢?
- 地址的本质就是数据,只不过这个数据有特殊的含义
- 保存这种特殊含义的数据,我们需要指针变量
8.2指针
如果一个指针类型的变量p
,保存了某个变量a
的地址,我们就称p
指向了a
int a; int *p = &a;
语法
基类型 *指针变量名{=地址};
- 基类型:指针指向的类型
*
:指示这是一个指针变量- 指针变量名:符合C语言标识符命名规则即可
指针变量的字节数:
- 指针变量的字节数是固定的,不随着指向类型的变化而变化,字节数一般与
long
类型相同(取决于编译器)。 - 指针的字节数决定了寻址范围。
8.3*
的含义
*
运算符读作 指针运算符/解引用运算符(指向运算符)
-
指针运算符:
int *p;
表示这是一个指针 -
解引用运算符:
int a; int *p = &a; //&a 引用;*(&a) 解引用 //*p == a; p == &a;
写一个函数,交换两个变量的值:
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } int main() { int a = 3; int b = 4; swap(&a, &b); }
8.4指针的初始化
int a; //a 空间中的值是未知的 int *p; //p 空间中的值是未知的 int a = 10; //将10 放到a 空间中 int *p = &a; //将a的地址,存放在p所对应的空间中 int *p = 10; //合法但不合理 int *p = (int *)10; //(int *)10 代表一个地址,指针这么指其实没什么意义
空指针:
int *p = NULL; //p为空指针,NULL表示空,也就是说这个东西合法,但不可访问. *p = 10; //访问会导致程序异常,段错误。
野指针:
int *p; //p 空间中的值是未知的 *p = 10; //访问大概率会导致程序异常,段错误。
- 建议指针变量进行定义时,如果没有具体的指向,请初始化为NULL;NULL是一种检测手段,可以检测这个指针是否被使用,可以通过
if(p == NULL)
判断指针是否被使用
8.5一维数组和指针
数组各个元素都有自己的地址,并且地址是连续的
int a[5] = {1, 2, 3, 4, }; //申请了五个整型元素的空间,并且将a和空间首地址绑定,并且可以用a来表示这个数组,也就是说我可以定义一个指针来指向这个数组 int *p = &a[0]; int *p = a; //这两种方法都可以让p指向a[0],*p == a[0]; //将a[0]设置为1的方法 a[0] = 1; *a = 1; *p = 1; p[0] = 1; //将a[1]设置为1的方法 a[1] = 1; *(a+1) = 1; *(p+1) = 1; p[1] = 1;
- 指针/地址与数值的加减,不再是数值的加减,而是加减基类型的字节数
- 指针/地址之间的加法/乘法/除法是毫无意义的。
- 指针/地址之间的减法(基类型相同的情况下),表示两个地址之间相差了多少个基类型
int a[5]; /* 数值上,a 和 &a[0] 和 &a 是相同的 a表示数组的首地址 &a[0]表示a[0]的地址 &a表示整个数组的地址 事实上,a的类型是int [5] */ int (*p)[5] = &a; //数组指针
8.6二维数组与指针
int a[3][4]; //二维数组可以看作3个一维数组,并且数组名为a[0] a[1] a[2] //所以a[0]的类型是int [4] int (*p)[4] = a; //此时p代表a //设置a[0][0]为1 a[0][0] = 1; p[0][0] = 1; *(a[0] + 0) = 1; *(*(p+0) + 0) = 1; //设置a[2][0]为1 a[2][0] = 1; p[2][0] = 1; *(a[2] + 0) = 1; *(*(p+2) + 0) = 1;
int a[3][4] /* &a a &a[0] &a[0][0] 在数值上是相同的,但含义不同 &a[0][0]:代表a[0][0]这个元素的地址 &a[0] :代表a[0]这个数组的地址 a :代表a[0]这个数组的地址 &a :代表a这个数组的地址 */
二维数组作为函数参数
int a[3][4]; int waterPools(int (*a)[4], int row, int col) int waterPools(int a[][4], int row, int col)
8.7指针数组与数组指针
int *a[4]; //指针数组 int (*a)[4]; //数组指针
int *a[4]
本质上是一个数组,这个数组内保存的都是地址int (*a)[4];
本质上是一个指针,可以指向一整个数组
指针数组的常见用法:
char *str[] = {"hello world", "123456", "abcdefg"}; //str[0] 存了"hello world"的地址 //str[1] 存了"123456"的地址 //str[2] 存了"abcdefg"的地址
作业:
1.输入一串小写字母,对小写字母按照"abcdefg"这种规则进行排序
#include <stdio.h> void letSort(char *p) { int i = 0; while(*(p+i) != '\0') { if(*p < 'a' || *p > 'z') { printf("please inpute letter\n"); return; } i++; } for(int j = 0; j < i-1; j++) { for(int k = j+1; k < i; k++) { if(*(p+j) > *(p+k)) { char t = *(p+j); *(p+j) = *(p+k); *(p+k) = t; } } } } int main() { char letter[100]; scanf("%s",letter); printf("%s\n", letter); letSort(letter); printf("%s\n", letter); }
2.输入一串字符,将其中重复的字符删除只剩一个
例如:输入 aaaaabbbbbddd1111ddd
输出 abd1
#include <stdio.h> void delChar(char *str, char *result) { int i = 0; int index = 0; int state = 0; while(*(str+i) != '\0') { i++; } for(int j = 0; j < i; j++) { for(int k = 0; k < i; k++) { if(*(str+j) == *(result+k)) { state = 1; break; } } if(state == 0) { *(result+index) = *(str+j); index++; } state = 0; } } int main() { char str[100] = {'\0'}; char result[100] = {'\0'}; gets(str); puts(str); delChar(str, result); puts(result); }
8.8const
关键字
const
:只读;被该关键字修饰的变量的值不能被改变
int a = 10; a = 11; //可以正常修改a的值 const int a = 10; /*等价*/ int const a = 10; a = 11; //不可以正常修改a的值
const
修饰指针:
int * const p; //const修饰指针变量p,p的值不能被改变,意味着p的指向不能被改变 int const *p; //const修饰*p,p指向的地址的值不能被修改,p的指向可以被修改 const int *p; //const修饰*p const int * const p; //const既修饰p又修饰*p
const
所添加的属性是伪只读属性:
const int a = 10; printf("%d\n", a); int *p = &a; *p = 20; printf("%d\n", a); //用指针是可以修改被const修饰的变量的值的
8.9二级指针和多级指针
保存一个普通变量的地址,我们可以用一个一级指针来保存,对于p来说,它本身的空间也有地址编码,我们要定义一个指针指向p,如何定义
int a; int *p = &a; //一级指针 int **pp = &p; //二级指针 int ***ppp = &pp; //三级指针
8.10字符串与指针
字符串就是一串以'\0'
结尾的字符集,C语言并没有字符串类型,在C语言中字符串的表现方式是指针/地址。
char *str = "hello world"; char str1[] = "hello world";
指针str
和数组str1[]
有没有区别
str
是一个指针,指向了保存"hello world"
空间的地址,str
指向的"hello world"
是只读的,修改会段错误str1[]
是一个数组,是用自己的空间保存了"hello world"
字符串
字符串的表示
- 以
" "
引起来的字符集,结尾有隐藏的'\0'
- 保存在字符数组中的带有
'\0'
的字符集char shr[] = {'h', 'e', 'l', 'l', 'o', '\0'};
8.11字符串相关函数
在C标准库中有一系列处理字符串的函数,声明一般在string.h
中
-
求字符串长度
strlen
#include <string.h> size_t strlen(const char *str); /* str:指针,指向待计算的字符串 返回值:字符串的长度 */ -
比较字符串是否相等
strcmp
strncmp
int strcmp(const char *s1, const char *s2); /* s1:指针,指向待比较的字符串 s2:指针,指向待比较的字符串 返回值: s1 == s2:0 s1 < s2 :负数 s1 > s2 :整数 */ int strncmp(const char *s1, const char *s2, size_t n); /* s1:指针,指向待比较的字符串 s2:指针,指向待比较的字符串 n:需要比较几个字符 返回值: s1 == s2:0 s1 < s2 :负数 s1 > s2 :整数 */ -
拷贝字符串
strcpy
strncpy
#include <string.h> char *strcpy(char *dest, const char *src); /* 拷贝src中的字符串包括'\0'到dest指向的空间中,注意dest的空间要足够大,以防止越界。 dest:指针,指向拷贝的目的空间 src:指针,指向拷贝的源空间 返回值:返回dest */ char *strncop(char *dest, const *src, size_t n); /* 拷贝src中的字符串至多n个字符到dest指向的空间中,注意dest的空间要足够大,以防止越界。 dest:指针,指向拷贝的目的空间 src:指针,指向拷贝的源空间 n:要拷贝多少个字符 返回值:返回dest */ -
字符串拼接
strcat
strncat
#include <string.h> char *strcat(char *dest, const char *src); /* dest:指针,指向被拼接的字符串空间 src:指针,指向要拼接的字符串 返回值:返回dest */ char *strncat(char *dest, const char *src, size_t n); /* dest:指针,指向被拼接的字符串空间 src:指针,指向要拼接的字符串 n:要拼接的字符的个数 返回值:返回dest */ -
字符串输入
gets
fgets
#include <stdio.h> char *gets(char *s); /* s:指针,指向保存被输入数据的空间 返回值:成功,返回s;失败,返回NULL。 */ char *fgets(char *s, int size, FILE *stream); /* s:指针,指向保存被输入数据的空间 size:要读取的数据的个数 stream:文件指针,从哪里获取数据 返回值:成功,返回s;失败,返回NULL。 */ -
字符串输出
puts
fputs
int puts(const char *s); /* s:指针,指向保存被输出数据的空间 返回值:输出成功返回非负整数,失败返回EOF */ int fputs(const char *s, FILE *stream); /* s:指针,指向保存被输出数据的空间 返回值:输出成功返回非负整数,失败返回EOF stream:文件指针,表示要输出的目标文件 */
作业:自己实现 strlen
strcpy
strcat
strcmp
strlen
#include <stdio.h> int myStrlen(char *str) { int len = 0; while(*(str+len)) len++; return len-1; } int main() { char str[100]; fgets(str, 100, stdin); int len = myStrlen(str); printf("strLen:%d\n", len); }
strcpy
#include <stdio.h> char *myStrcpy(char *dest, char *src) { int i = 0; do { *(dest+i) = *(src+i); i++; }while(*(src+i)); return dest; } int main() { char src[] = "hello world"; char dest[100] = {0}; myStrcpy(dest, src); printf("%s\n", dest); }
strcat
#include <stdio.h> char *myStrcat(char *dest,const char *src) { int i = 0, j = 0; while(*(dest+i)) i++; do { *(dest+i) = *(src+j); i++; j++; }while(*(src+j)); } int main() { char src[] = "hello world"; char dest[100] = "world hello"; printf("%s\n%s\n", src, dest); myStrcat(dest, src); printf("%s", dest); }
strcmp
#include <stdio.h> int myStrcmp(const char *s1, const char *s2) { int i = 0; do { if(*(s1+i) < *(s2+i)) return -1; else if(*(s1+i) > *(s2+i)) return 1; i++; }while(*(s1+i)); return 0; } int main() { char s1[100]; char s2[100]; int compare; printf("please input str1\n"); fgets(s1, 100, stdin); printf("please input str2\n"); fgets(s2, 100, stdin); compare = myStrcmp(s1, s2); printf("%d\n", compare); }
8.12函数指针
C程序中,每一个函数都有函数空间,每个函数空间都有一个地址,这个地址叫函数的入口地址(函数名代表了函数的首地址)
顾名思义,函数指针就是指向函数的指针。
函数的类型
void func(void); //func的类型:void(void) char *strcpy(char *dest, char *src); //strcpy的类型:char *(char *, char *)
定义一个指针指向函数
//指向函数strcpy char *(char *, char *) *pstrcpy = strcpy;//不是这么写的 //变成 char *(*pstrcpy)(char *, char *) = strcpy; //或者 char *(*pstrcpy)(char *, char *) = &strcpy;
函数指针的使用
char *str = "hello"; char buf[10] = {0}; pstrcpy(buf, str); //或者 (*pstrcpy)(buf, str);
练习:
1.定义一个函数指针指向gets函数,利用该函数指针从终端获取一串字符并输出
#include <stdio.h> int main() { char str[100]; char *(*pGets)(char *); pGets = gets; pGets(str); printf("%s", str); }
2.利用函数指针实现一个既可以做整数加法,也可以做小数加法的函数
函数指针的使用场景
回调函数
....
示例:
假设我们有一个排序库,这个库里面有很多中实现排序的方法(冒泡、选择、快排....),现在需要实现一个函数,这个函数的功能就是排序,要求使用排序库中的函数来实现排序
//以下是排序库中的函数的声明 void BubbleSort(int *a, int n); void InserSort(int *a, int n); void SelectSort(int *a, int n); void NumSort(int *a, int n); void QuickSort(int *a, int n); void Sort(int *a; int n, void(*func)(int *, int)) { func(a, n); } int main() { int a[5] = {1, 3, 7, 5, 6}; sort(a, 5, QuickSort); }
函数指针数组:保存函数指针的数组。
//函数声明 void func1(void); void func2(void); void func3(void); //定义函数指针数组 void (*a[3])(void); //初始化函数指针 a[0] = func1; a[1] = func2; a[2] = func3; //利用函数指针调用函数 a[0](); a[1](); a[2]();
函数指针数组指针:指向函数指针数组的指针
void (*(*p)[3])(void) = &a;
指针函数:
返回值类型为指针的函数
char *strcpy(char *dest, char *src);
8.13动态内存分配
int a[10] = {0};
a所代表的内存,是固定长度的,没办法变化
假设我们可以将内存的长度进行变化的申请,如果有一组数据一开始需要200字节,那我们定义内存大小为200字节,后来又需要400字节,我们又可以申请400个字节。
动态内存实际上是一块空间,这块空间随着需要而被申请相应的字节数,当不需要的时候,可以释放这块空间。
动态内存的特点:
- 手动申请,手动释放。
- 生存周期随进程持续(只要没有手动释放)
- 作用域全局
动态内存的获取:有三个函数 malloc
calloc
realloc
释放:free
void *malloc(size_t size); /* size:要申请的字节数 返回值:成功,返回动态内存空间的地址,如果size为0,返回NULL; 失败,返回NULL; malloc分配的空间,空间中的内容未知 */ void free(void *ptr); /* ptr:要释放的动态内存的地址 返回值:无 */ void *calloc(size_t nmemb, size_t size); /* nmemb:数组元素的个数 size:每个元素所占的字节数 返回值:成功,返回动态内存空间的地址,如果size为0,返回NULL; 失败,返回NULL; calloc分配的空间,会全部初始化为0 */ void *realloc(void *ptr, size_t size); /* 对原来的空间重新分配 ptr:要重新分配的空间 size:重新分配后的空间大小 返回值:成功,返回动态内存空间的地址,如果size为0,返回NULL; 失败,返回NULL; */
- 内存的申请和释放要成对出现,否则会造成内存泄漏的问题
void *
:通用指针
void a; //不合法,计算机不知道用什么方式解析a void *p; //合法,指针的长度恒为机器位数对应的字节数,如果不知道指针将来会指向什么,可以使用void *
8.14 main
函数的参数问题
main
函数也是可以进行参数传递的,main
函数的实参由终端给出,实参类型是字符串
int main(int argc, char *argv[]) { } /* argc:参数个数 argv:指针数组,保存实参字符串的首地址 */
在终端输入./a.out
./a.out
就是argv[0]
本文作者:乐情在水静气同山
本文链接:https://www.cnblogs.com/aalynsah/p/17533222.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步