目录

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来说,(01)其实是一个逗号表达式,它的结果是最后一个元素的值,即(01==1。所以其实在数组arr里只放入了{135}.所以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的起始地址
  • 模拟实现
    版本一:
    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