C语言学习笔记week22-24-3-2
目录
C语言学习笔记(week2)2-24 3-2
22 数组指针变量
- 指向整个数组的指针
int(*p)[5]
其中(*p)说明p是一个指针变量,[5]说明p指向的是一整个数组,5代表元素个数,int 代表指向这个数组里边存放的元素的数据类型int arr[6] = { 1,2,3,4,5,6 }; int (*p)[6] = &arr; for (int i = 0; i < 6; i++) { printf("%d\n", (*p)[i]); }
- 对于数组指针,如果想访问数组里的每一个元素,需要像上述代码一样(即*p相当与数组名)
23 二维数组传参
对于二维数组本质的理解: 二维数组的每一行其实都是一个一维数组,也就是说二维数组可以理解成是一个一维数组的数组,二维数组的每一个元素都是一个一维数组,二维数组的数组名是首元素的地址,也就是第一行的地址(就是这个一维数组的地址)
void my_print2(int(*arr)[5], int l, int r) { for (int i = 0; i < l; i++) { for (int j = 0; j < r; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; my_print2(arr, 3, 5); return 0; }
对于
p[i][j]
来说,相当于*( *(p+i)+j )
24 函数指针变量
- 指向函数的指针,存放的是函数的地址。(函数也是有地址的,函数名就是函数的地址,并且&Add与Add没有任何区别,都是函数的地址)
- 基本格式
int (*p)(int, int) = &Add
(*p)说明是指针,(int,int)是函数参数,说明指向的是一个函数,int是函数的返回值类型 - 基本用法
int res = (*p)(1, 3)
对指针解引用就相当于得到了函数名。int res = p(1, 5);
其实也可以,也就是可以直接用函数地址来进行调用,但推荐使用第一种,易于理解 (* ( void(*) ( ) ) 0 ) ()
对于这个代码的理解:void(*)()
是一种函数指针类型,它指向的函数没有参数,返回值为void(void(*)())0
在括号里边放类型是强制类型转化,也就是将0这个int类型的数据转变为一个函数指针类型(也就是将0看作一个地址)*(void (*)())0
相当于对一个函数指针解引用,得到的是一个函数名(* ( void(*) ( ) ) 0 ) ()
相当于调用这个函数 整个表达式的目的就是调用地址为0处的函数
25 typedef关键字
- 可以给类型重命名
- 格式:
typedef long long int LL;
- 在连续创建多个指针变量时,应写为
int* p1,*p2
,每个指针变量都有自己的*。 - 对于函数指针重命名的时候,应该
typedef void (*Pt)(int) ;
只能把重命名的名字(Pt)写到里边
26 函数指针数组
- 一个数组里面存放的元素都是函数指针。
- 格式:
int (*arr[4])(int,int)={Add,Sub};
其中Add,Sub都是函数名称。其实写作int (*)(int,int) arr[4] ={Add,Sub};
更容易接受,但语法规定 - 注意,只有多个函数有相同的返回值类型,以及相同的参数类型的时候,多个函数名才能放入同一个函数指针数组里面。
- 应用:转移表(可以大大简化代码,可以起到跳转的作用)
void menu() {
printf("********************\n");
printf("1.add 2.sub\n");
printf("3.mul 4.div\n");
printf(" 0.exit\n");
printf("********************\n");
}
int add(int x,int y) {
return x+y;
}
int sub(int x, int y) {
return x -y;
}
int mul(int x, int y) {
return x*y;
}
int div(int x, int y) {
return x/y;
}
int main() {
int input = 0;
int (*parr[5])(int, int) = { NULL,add,sub,mul,div };
do {
menu();
printf("请选择一个数字\n");
scanf("%d", &input);
if (input == 0) {
printf("退出程序\n");
}
else if (input > 0 && input < 5) {
printf("请输入两个操作数:");
int a, b;
scanf("%d %d", &a, &b);
int res=(parr[input])(a, b);
printf("res=%d\n", res);
}
else {
printf("请重新输入\n");
}
} while (input);
return 0;
}
27 回调函数
- 回调函数是指通过函数指针而被间接调用的函数。相当于将某一个函数作为参数在函数间进行传递。(在某些时候可以大大简化代码)
- qsort函数的使用技巧(回调函数的例子,可以对任何数据类型进行排序)
void qsort(void *base, size_t num, size_t size, int (*cmp)(const void *, const void *));
其中base
指向待排序数组,num
待排序数组元素个数,size
待排序数组中每个元素所占内存的大小(单位字节),cmp
是一个函数指针,指向一个函数,这个函数可以用来比较base数组里任意两个元素的大小。比较两个整数 int cmp_int(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; } //因为void*不能直接解引用,所以必须先进行强制类型转化 int main() { int arr[] = { 6, 3, 9, 8, 5, 2, 4, 7, 1, 0 }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, 4, cmp_int); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }
比较两个结构体 struct Stu { char name[20]; int age; }; int cmp_struct(const void*p1,const void*p2){ return strcmp((*(struct Stu*)p1).name, (*(struct Stu*)p2).name); } //因为void*不能直接解引用,所以必须先进行强制类型转化 int main() { struct Stu arr[3] = { {"bbb",70},{"ccc",18},{"aaa",100} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_struct); for (int i = 0; i < 3; i++) { printf("%s ", arr[i].name); } return 0; }
qsort函数的模拟实现 (核心在于无论什么类型的数据,都可以一个一个字节的交换)
int cmp_int(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; //return *(int*)p2 - *(int*)p1 ; } void my_swap(char* p1,char* p2,size_t n) { for (int i = 0; i < n; i++) { char tem = *p1; *p1 = *p2; *p2 = tem; p1++; p2++; } } void bubble_qsort(void * base,size_t sz,size_t width,int(*cmp)(const void *,const void *)) { for (int i = 0; i < sz - 1; i++) { for (int j = 0; j < sz - 1; j++) { if (cmp(((char*)base + j * width), ((char*)base + (j+1)*width)) > 0) { my_swap(((char*)base + j * width), ((char*)base + (j + 1) * width),width); } } } } int main() { int arr[] = { 6, 3, 9, 8, 5, 2, 4, 7, 1, 0 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_qsort(arr, sz, 4, cmp_int); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }
28 sizeof 和 strlen
- sizeof是一种操作符,它是根据字符串所占内存的大小来计算字节个数的(因为char是一个字节,所以也是字符个数),所以无论有没有
\0
,计算结果没有区别。并且结果的个数是包含\0
在内的个数 - strlen是一种函数根据
\0
来进行计数的,它会返回\0
之前所有字符的个数,不包含\0
。如果没有\0
,则会一直向后查找,直到遇到\0
为止。所以有可能造成越界访问
29 关于指针的易错题
对于数组名,当
sizeof(数组名)
只有数组名时,代表整个数组的大小。但如果数组名和其他元素或数字一起放入()里,则表示首元素的地址,不在是那种特例了arr[4]={1,2,3,4}; sizeof(arr);//结果是16 sizeof(arr+0);//结果是4/8 因为arr不是单独在里面,所以只能表示首元素地址(地址的大小取决与x86/x64)
对于srtlen(arr)来说,它接受的参数必须是一个指针,所以无论传进去什么,都会被当作指针对待,在函数内部会对它进行解引用操作。所以不能传入一个int等类型所以
strlen(*arr)
是一个错误语句。int a[3]][4]={0}; sizeof(a[0]) a[0]相当于第一行首元素的地址,现在单独放在了sizeof()里边,代表的是整个第一行,所以答案是4*4 sizeof(a[0]+1) 现在a[0]不是单独放在sizeof()里面,所以a[0]只表示第一行第一个元素的地址,+1之后变成a[0][1]的地址 sizeof(a+1) a不是单独放在sizeof()内部,所以表示的是首元素的地址,即第一行的地址,所以a+1表示第二行的地址,所以结果是4/8(因为其仍然是一个地址) sizeof() 这个操作符在计算大小的时候,是根据类型推断的,不直接去访问所传入的空间,所以哪怕传入了一个野指针,也不造成越界访问,仅仅根据类型判断。
在x86条件下,该结构体大小是20个字节 struct test{ int a; char* b; short c; short d[4]; char e[2]; }* p=(struct test*)0x100000; 1.p+0x1 2.(unsigned int*)p + 0x1 3.(unsigned long)p + 0x1 1.因为P是一个结构体指针类型,所以当+1之后跳过一个结构体的大小(即20个字节,答案为0x100014(16进制)) 2.因为p被强转成了int*类型,所以p+1之后跳过一个整型的大小,答案为0x100004(16进制)) 3.p被强转成long 类型之后,p就从一个指针变量变成了一个long型的变量,+1就相当与两个数字之间进行运算,所以答案为0x100001(16进制))
int arr[3][2]={(0,1),(2,3),(4,5)} int* p; p = a[0]; printf("%d\n",p); 对于数组arr来说,(0,1)其实是一个逗号表达式,它的结果是最后一个元素的值,即(0,1)==1。所以其实在数组arr里只放入了{1,3,5}.所以p是arr第一行第一个元素的地址。
1 字符分类函数
都在头文件< ctype.h > 里面
2 字符转换函数
- 都在头文件< ctype.h > 里面
- tolower(),toupper();大小写转化函数
3 strlen 函数的使用和模拟实现
- strlen的返回值是size_t,是一种无符号整数。(对于无符号整数来说,是没有负数的,如果一个负数被转变为无符号整数,会直接将这个负数的补码看作一个无符号整数,也就是会变成一个很大的正数(因为负数首位符号位是1))
模拟实现(不创建临时变量)(使用递归) size_t my_strlen(const char* str) { if (*str != '\0') { return 1 + my_strlen(str + 1); } else { return 0; } } int main() { char arr[] = "abcdefaaa"; size_t len = my_strlen(arr); printf("%zd ", len); return 0; }
4 strcpy 的使用和模拟实现
strcpy(arr2, arr1)
将arr1的内容放入arr2里- 在拷贝时,arr1里面必须包含
\0
- 再拷贝时
\0
也会被拷贝进入arr2 - 如果arr2里面原本有元素,那么arr2的元素会被arr1的元素从头开始覆盖,为覆盖的元素保持不变
- strcpy有返回值是一个指针,指向arr2的起始地址
- 在拷贝时,arr1里面必须包含
模拟实现 版本一: void my_strcpy(char* dest,char* src) { while (*src != '\0') { *dest = *src; dest++; src++; } *dest = *src; } 版本二: char* my_strcpy(char* dest, char* src) { char* res=dest; assert(dest&&src); while (*dest++ = *src++) { ; } return res; } int main() { char arr1[] = "asjdflks"; char arr2[20]; my_strcpy(arr2, arr1); printf("%s", arr2); return 0; }
5 strcat使用及其模拟实现
- 使用:在给定字符串后面追加另一个字符串
- 注意:目标和源头字符串都要包含‘\0’,
strcat(char * destination,const char* source)
- 模拟实现
char* my_strcat(char * dest,const char* src) { char* res = dest; while (*dest != '\0') { dest++; } while (*dest++ = *src++) { ; } return res; } int main() { char arr[20] = "hello "; char arrr[] = "world"; my_strcat(arr, arrr); printf("%s", arr); return 0; }//(注意,不可以追加两个相同的字符串,不然会因为‘\0’的丢失造成死循环)
6 strcmp的使用与模拟实现
- 比较的不是长度,是对应字符的ASCII码的大小
- 在传递时,如果传递的是“abc”,直接传递这样的常量字符串,也是可以使用char*的指针接收的。
- 模拟实现
int my_strcmp(const char* str1,const char*str2) { assert(str1 && str2); while (*str1 == *str2) { if (*str1 == '\0') { return 0; } str1++; str2++; } return (*str1 - *str2); } int main() { int res = my_strcmp("ax", "ade"); printf("%d", res); return 0; }
7 strncmp strncpy strncat可以指定长度的字符串函数
8 strstr的使用和模拟实现
- 在字符串里查找一个字串第一次出现的地方
const char* strstr(const char* st1,const char* str2)
在str1里找str2- 模拟实现
char* my_strstr(const char* str1,const char*str2) { char* s1 = NULL; char* s2 = NULL; char* cur = str1; while (*cur) { s1 = cur; s2 = str2; while (*s1 && *s2 && *s1 == *s2) { s1++; s2++; } if (*s2=='\0') { return cur; } cur++; } return NULL; } int main() { char arr[] = "abcsdfhdf"; char arrr[] = "sd"; char* p = my_strstr(arr, arrr); printf("%s", p); return 0; }
9 strtok函数的使用
- 用于分割字符串(以所提供的字符为分隔符)
char* strtok(char* str,char* sep)
sep指向一个字符串,包含所有用于分割字符串的字符- 如果待切割的字符串里有多个分割符,则调用一次该函数,会将从左边数第一个分隔符变成’\0’,并且每一次调用该函数会存下来这次操作到的位置,下一次在调用的时候,会直接从这个位置开始向后寻找下一个分隔符。除了第一次调用,其他时候调用时,str只需要传入NULL即可
示例: int main() { char arr[] = "zpw@bitedu.net"; char a[] = "@."; for (char* r = strtok(arr, a); r != NULL;r=strtok(NULL,a)) { printf("%s\n", r); }//用于分割字符串 return 0; }
- 应该是内部实现的时候有一个 static 修饰的变量(只在第一次调用函数时初始化,之后再次调用该函数时,不会再次初始化,而是保留上一次调用结束时的值。)
10 strerror的使用
- 每⼀个错误码( 库函数的错误码 )都是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。
char* strerror()
可以用于查看错误信息,在errno变量里for (int i = 0; i < 20; i++) { printf("%d:%s\n", i, strerror(i)); }
11 perror函数
void perror(char* str)
它这个函数会自动打印错误信息,这个str参数是一个提示用的信息,自己输入的perror(“错误信息是:”)
打印结果就是:错误信息是:No such file or directory