指针
part 1
1.内存和地址
计算机的CPU在处理数据时,需要的数据是从内存中读取的,处理后的数据也会放回内存中。
内存划分为一个个的内存单元,每个内存单元大小取1个字节(8bit)。每个内存单元的编号称为地址。
【在C语言中,地址就是指针 内存单元编号==地址==指针】
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存中的位置,但内存中字节众多,所以需要进行编址(通过硬件完成)。计算机内有很多的硬件单元互相协同工作(这里的协同指至少互相之间能够进行数据传输),硬件与硬件之间相互独立,通过“线”连通。
例如,32位的机器有32根地址总线,每根线只有两种状态,表示0,1(电脉冲有无),那么一根线可以表示两种含义,两个线就能表示4种含义,32根地址线,就可以表示出2的32次幂的含义,每种含义就对应着一个地址。
地址信息被下达给内存,在内存上就可以找到该地址对应的数据,将数据通过数据总线传入CPU内的寄存器。
2.指针变量和地址
2.1 取地址操作符 &
在C语言中,创建变量就是向内存申请空间
#include<stdio.h> int main() { int a = 10; return 0; }
上述代码就是创建了整型变量a,内存中申请了4个字节,用于存放整数10,其中每个字节都有地址。
用代码实现读取变量a的地址:
#include<stdio.h> int main() { int a = 10; &a; printf("%p\n",&a); return 0; }
&a取出的是a所占4个字节中地址较小的字节的地址。
2.2 指针变量和解引用操作符(*)
指针变量
通过&拿到的地址是一个数值,比如:0x006FFD70,当需要存储这个数值以便后期使用时,要将其存放在指针变量中。
#include<stdio.h> int main() { int a = 10; int* pa = &a; return 0; }
【指针变量也是一种变量,用来存放地址,存放在指针变量中的值都会理解为地址。】
拆解指针类型
int a = 10; int* pa = &a;
int说明pa指向的是整型类型的对象,*说明pa是指针变量。
同理,对于char类型
char ch = 'strive'; char* ch = &ch;
解引用操作符
在C语言中拿到地址(即指针),就可以通过地址找到地址指向的对象,这里需要解引用操作符的参与。
#include<stdio.h> int main() { int a = 100; int* pa = &a; *pa = 0; return 0; }
在“*pa = 0;”中就应用了解引用操作符, *pa的意思就是通过pa中存放的地址找到对应空间,更改其中的数据,此语句执行后,就将a的值改为了0.
2.3 指针变量的大小
指针变量的大小取决于地址的大小
-
32位平台下地址是32个bit位(4字节);
-
64位平台下地址是64个bit位(8字节);
#include<stdio.h> int main() { printf("%zd\n",sizeof(char *)); printf("%zd\n",sizeof(short *)); printf("%zd\n",sizeof(int *)); printf("%zd\n",sizeof(double *)); return 0; } //X86环境输出结果:4 4 4 4 //X64环境输出结果:8 8 8 8
结论:
-
32位平台下地址是32个bit位,指针变量大小是4字节;
-
64位平台下地址是64个bit位,指针变量大小是8字节;
-
【指针变量的大小和类型无关,只要是指针类型的变量,在相同平台下大小都相同】
指针变量类型的定义
指针的解引用
//代码1 #include<stdio.h> int main() { int n = 0x11223344; int* pi = &n; *pi = 0; return 0; } //代码2 #include<stdio.h> int main() { int n = 0x11223344; char* pc =(char*) &n; *pc = 0; return 0; }
上述两个代码经过调试会发现:代码1会将n的4个字节全部改为0,但代码2只是将n的第一个字节改为0.
结论:
指针的类型决定了【对指针解引用时的权限大小(即一次能操作几个字节)】
3.2 指针+-整数
#include<stdio.h> int main() { int n = 10; char *pc = (char*)&n;//强制转换了一下 int *pi = &n; printf("%p\n",&n);//00AFF974 printf("p\n",pc);//00AFF974 printf("p\n",pc+1);//00AFF975 printf("p\n",pi);//00AFF974 printf("p\n",pi+1);//00AFF978 return 0; }
上述代码运行结果表明:char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过4个字节。
-1同理。
结论:
指针的类型决定了指针向前或者向后移动的空间大小。
3.3 void*指针
void*指针类型可以理解为无具体类型的指针,或者叫泛型指针,这种类型的指针可以用来接受任意类型地址。
【void* 具有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算操作。】
#include<stdio.h> int main() { int a = 10; int* pa = &a; char* pc = &a; return 0 }
在上述代码中,将int类型的变量地址赋给char* 类型的指针,会导致编译器发出警告,因为类型不兼容,使用 void*类型就不会出现这种问题。
#include<stdio.h> int main() { int a = 10; void* pa = &a; void* pc = &a; *pa = 10; *pc = 0; return 0; }
编译这段代码,也会发生报错,会显示“void* 与int的间接级别不同 ”,这时因为【void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算】
一般void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。
4.const修饰指针
4.1 const修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量的也可以修改这个变量,当需要给一个变量加上一些限制,使之不可修改,就要使用const。
代码1:
#include<stdio.h> int main() { int m = 0; m = 20;//此时,m变为20 const int n = 0;//不可修改 n = 20; return 0; }
代码2:
#include<stdio.h> int main() { const int n = 0; printf("n = %d\n",n); int* p = &n; *p = 20; printf("n= %d\n",n); return 0; }
代码2中绕过了n,使用了n的地址去修改n值,但是这样违背了让n不可修改的本意,所以要用const修饰指针变量。
4.2 const修饰指针变量
一般来讲const修饰指针变量可以放在 * 的左边或右边,表示含义不同。
int const * p; int * const p;
代码举例具体分析:
#include<stdio.h> void test1() { int n = 10; int m = 20; int *p = &n; *p = 20; p = &m; } void test2() { int n = 10; int m = 20; const int* p = &n; *p = 20; p = &m; } void test3() { int n = 10; int m = 20; int* const p = &n; *p = 20; p = &m; } void test4() { int n = 10; int m = 20; int const * const p = &n; *p = 20; p = &m; } int main() { test1(); test2(); test3(); test4(); return 0; }
结论:
-
const放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但指针变量本身的内容可变(即地址可变);
-
const放在*右边,修饰的是指针变量本身,保证了指针变量的内容不能修改(地址不可变),但是指针指向的内容可以通过指针改变。
5. 指针运算
指针的三种基本运算:
-
指针+-整数
-
指针-指针
-
指针的关系运算
5.1 指针+-整数
数值在内存中是连续存放的,只要知道第一个元素的地址,就可以找到后面的所有元素。
#include<stdio.h> int main() { int a[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &a[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i = 0; i < sz; i++) { printf("%d ",*(p+i)); } return 0; }
5.2 指针-指针
-
指针 - 指针 得到的是两个指针之间的元素个数
#include<stdio.h> int my_strlen(char *s) { char *p = s; while(*p != '\0') p++; return p-s;//此时就是最后一个字符的地址减去第一个字符的地址。 } int main() { printf("%d\n",my_strlen("abc")); return 0; }
5.3 指针的关系运算
-
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
#include<stdio.h> int main() { int a[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &a[0]; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) { printf("%d ", *p); p++; } return 0; }
6. 野指针
概念:野指针就是指针指向的位置不可知(随机的、不正确的、没有明确限制的)。
6.1 野指针成因
1.指针未初始化
#include<stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
2.指针越界访问
#include<stdio.h> int main() { int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0;i<=11;i++) { *(p++) = i;//当指针指向的范围超出数组arr的范围时,p就是野指针。 } return 0; }
3.指针指向的空间释放
#include<stdio.h> int* test() { int n = 100; return &n; } int main()//我们想通过*p来改a的值,但是当出了test()函数,a就被销毁了,所以这里的p是野指针 { int *p = test(); printf("%d\n", *p); return 0; }
6.2 如何规避野指针
1.指针初始化
明确指针指向位置,直接赋值地址,如果不知道指针指向位置,可以给指针赋值NULL。
#include<stdio.h> int main() { int num = 10; int *p1 = # int *p2 = &NULL; return 0; }
2.小心指针越界
指针只能访问一个程序向内存申请了的空间,不能超出范围访问,超出了就是越界访问。
3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性。
4.避免返回局部变量地址
7. assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
assert(p!=NULL);//如果p不等于NULL程序继续运行,等于则终止
assert()宏 接受一个表达式作为参数,如果该表达式为真,assert()不会产生任何作用(小透明),程序照常执行。如果表达式为假,assert()直接一个报错(掀桌子),在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
如果已经确定了程序没有问题,不需要再做断言,就可以再头文件语句前定义一个NDEBUG。
#define NDEBUG #include<assert.h>
然后重新编译程序,编译器就会禁用文件中所有的assert()语句,出现问题先移除#define NDEBUG语句,再次编译可重新启用assert()语句。
assert()语句的缺点是引用了额外的检查,增加了程序的运行时间。
8.指针的使用和传址调用
8.1 strlen的模拟实现
库函数strlen的功能是求字符串长度,统计的是字符串中\0之前字符的个数。
size_t strlen(const char * str);
参数str接受一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度。如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,到\0停止。
int my_strlen(const char * str) { int count = 0; assert(str); while(*str) { count++; str++; } return 0; } int main() { int len = my_strlen("abcdef"); printf("%d\n",len); return 0; }
8.2 传值调用和传址调用
写一个交换两个整型变量的值的代码
#include<stdio.h> void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d",&a,&b); printf("交换前:a=%d b=%d\n",a,b); Swap1(a,b); printf("交换后:a=%d b=%d\n",a,b); return 0; }
运行之后,两个变量的值并未交换,因为形参与实参直接只是值的调用,在原本地址的内容未曾改变。这就是传值调用。
【结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。】
#include<stdio.h> void Swap2(int*px, int*py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d",&a,&b); printf("交换前:a=%d b=%d\n",a,b); Swap2(&a,&b); printf("交换后:a=%d b=%d\n",a,b); return 0; }
这种函数调用方式叫:传址调用。
传址调用可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。
part 2
1.数组名的理解
int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0];
上述代码使用&arr[0]拿到了数组第一个元素的地址,但是数组名本身就是地址且是数组首元素的地址
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }
上述代码两个输出的结果完全相同,总结:【数组名就是数组首元素的地址】
#include<stdio.h> { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("%d\n",sizeof(arr)); return 0; }
这个代码输出的结果并不是4/8(存储单元长度)
注意:
-
sizeof(数组名),sizeof中单独放数组名,这里的数组名就会表示整个数组,计算整个数组的大小,单位是字节;
-
&数组名,这里的数组名也表示整个数组,取出的是整个数组的地址。
-
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); printf("&arr = %p\n", &arr); return 0; }
在这个代码输出的结果发现三个输出一模一样,下面代码将表明区别。
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("&arr[0] = %p\n", &arr[0]); printf("&arr[0]+1 = %p\n", &arr[0]+1); printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr); printf("&arr+1 = %p\n", &arr+1); return 0; } /*输出结果: &arr[0] = 0077F820 &arr[0]+1 = 0077F824 arr = 0077F820 arr+1 = 0077F824 &arr = 0077F820 &arr+1 = 0077F860 &arr+1跳过了整个数组空间 */
使用指针访问数组
代码1:
#include<stdio.h> int main() { int arr[10] = {0}; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); int* p = arr; for(i=0;i<sz;i++) { scanf("%d",p+i); } for(i=0;i<sz;i++) { printf("%d ",*(p+i)); } return 0; }
代码2:
#include<stdio.h> int main() { int arr[10] = {0}; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); int* p = arr; for(i=0;i<sz;i++) { scanf("%d",p+i); } for(i=0;i<sz;i++) { printf("%d ",p[i]); } return 0; }
代码1和代码2都可以完成目的输出,本质上p[i]是等价于*(p+i)
同理arr[i]等价于*(arr+i),数组元素的访问在编译器处理时,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。
3.一维数组传参的本质
#include<stdio.h> void test(int arr[]) { int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n",sz2); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n",sz1); test(arr); return 0; }
在这段代码中,调用的test函数内部并没有正确获得数组的元素个数,因为:数组名是数组首元素的地址,在传递时本质上数组传参传递的是数组首元素的地址。
所有函数形参的部分【理论上应该使用指针变量来接收首元素的地址】
void test(int arr[]) { printf("%d\n",sizeof(arr)); } void test(int* arr) { printf("%d\n",sizeof(arr)); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; test(arr); return 0; }
总结:一维数组传参,形参的部分可以写出数组的形式,也可以写成指针的形式。
冒泡排序
核心思想:两两相邻的元素进行比较
方法1:
void bubble_sort(int arr[], int sz) { int i = 0; for(i=0;i<sz-1;i++) { int j = 0; for(j=0;j<sz-i-1;j++) { if(arr[j]>arr[j+1]) { int tmp = arr[j]; arr[j] = a[j+1]; arr[j+1] = tmp; } } } } int main() { int arr[] = {3,1,7,5,8,9,0,2,4,6}; int sz = sizeof(arr)/sizeof(arr[0]); bubble_sort(arr,sz); int i = 0; for(i=0;i<sz;i++) { printf("%d ",arr[i]); } return 0; }
方法2(优化):
void bubble_sort(int arr[], int sz) { int i = 0; for(i=0;i<sz;i++) { int flag = 1; int j = 0; for(j=0;j<sz-i-1;j++) { if(arr[j]>arr[j+1]) { flag=0; int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } if(flag==1) break; } } int main() { int arr[] = {3,1,7,5,8,9,0,2,4,6}; int sz = sizeof(arr)/sizeof(arr[0]); bubble_sort(arr,sz); int i = 0; for(i=0;i<sz;i++) { printf("%d ",arr[i]); } return 0; }
5.二级指针
指针变量自身也是变量,是变量就有地址,存放指针变量地址的指针就是二级指针。
int main() { int a = 10; int * pa = &a; int ** ppa = &pa; return 0; }
对于二级指针的运算:
*ppa通过对ppa中的地址进行解引用,找到pa, *ppa访问的就是pa;
int b = 20; *ppa = &b;//等价于 pa = &b
**ppa通过 * ppa找到pa,然后对pa进行解引用操作:*pa找到a。
**ppa = 30;//等价于*pa=30等价于a=30
6.指针数组
整型数组存放整型数,字符数组存放字符,指针数组就是存放指针的数组。
7.指针数组模拟二维数组
#include<stdio.h> int main() { int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; int* parr[3] = {arr1,arr2,arr3}; int i = 0; int j = 0; for(i=0;i<3;i++) { for(j=0;j<5;j++) { printf("%d ",parr[i][j]); } printf("\n"); } return 0; }
parr[i]是访问parr数组的元素,找到 的元素指向了整型一维数组,parr[i] [j]就是整型一维数组中的元素。
part 3
1.字符指针变量
在指针的类型中有一种指针类型为字符指针char*
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
int main() { const char* pstr = "hello"; printf("%s\n",pstr); return 0; }
代码const char* pstr = "hello";本质上是将字符串“hello”的首字符地址放到了字符指针pstr中。即将常量字符串的首字符h地址存放到指针变量pstr中。
#include<stdio.h> int main() { char str1[] = "hello"; char str2[] = "hello"; const char *str3 = "hello"; const char *str4 = "hello"; if(str1 == str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 == str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0; }
输出的结果显示:
str1 and str2 are not same
str3 and str4 are same
str3和str4指向的是同一个常量字符串,但是用相同的常量字符串去初始化不同的数组时就会开辟出不同的内存块,所以str1和str2不同。
2.数组指针变量
2.1 定义
之前提及了指针数组,存放的是地址(指针);而数组指针变量是一种指针变量。
-
整型指针变量:int * pi, 存放的是整型变量的地址,能够指向整型数据的指针;
-
浮点型指针变量:float * pf,存放浮点型变量的地址,能够指向浮点型数据的指针。
数组指针变量是:存放数组的地址且能够指向数组的指针变量。
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,所以p是一个指向数组的指针。
注:[]的优先级高于*,所以要加()
2.2 数组指针变量初始化
int arr[10] = {0}; &arr;
如果要存放数组的地址,就得存放在数组指针变量中:
int (*p)[10] = &arr; //int表示p指向数组的元素类型 //p是数组指针变量名 //[10]指元素个数
3.二维数组传参本质
之前使用二维数组需要传参时
#include<stdio.h> void test(int a[3][5],int r,int c) { int i = 0; int j = 0; for(i= 0;i<r;i++) { for(j= 0;j<c;j++) { printf("%d ",a[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}}; test(arr,3,5); return 0; }
【二维数组传参本质上也是传递地址,传递的是第一行一维数组的地址,所以形参也可以写成指针形式】
#include<stdio.h> void test(int (*p)[5],int r,int c) { int i = 0; int j = 0; for(i= 0;i<r;i++) { for(j= 0;j<c;j++) { printf("%d ",a[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}}; test(arr,3,5); return 0; }
总结:二维数组传参,形参的部分可以写成数组或指针形式
4.函数指针变量
4.1 函数指针变量的创建
首先利用一个代码测试函数是否有地址:
#include<stdio.h> void test() { printf("xixi\n"); } int main() { printf("test: %p\n",test); printf("&test: %p\n",&test); return 0; }
输出结果发现函数确实具有地址,并且函数名就是函数的地址,如果要将地址存放起来,就得创建函数指针变量:
void test() { printf("hello from the other sideeeee!\n"); } void(*pf1)() = &test; void(*pf2)() = test; int Add(int x,int y) { return x+y; } int(*pf3)(int,int) = Add; int(*pf3)(int x,int y) = &Add;
函数指针类型解析:
//对于 int (*pf3) (int x,int y) //int表示指针所指函数的返回类型 //*pf3 是函数指针变量名 //(int x,int y)表示函数的参数类型和参数个数
4.2函数指针变量的使用
#include<stdio.h> int Add(int x,int y) { return x+y; } int main() { int (*pf3)(int,int) = Add; printf("%d\n",(*pf3)(2,3)); printf("%d\n",pf3(3,5)); return 0; } //输出结果为:5 8
4.3 两段代码
(*(void(*)())0)();//代码1 void (*signal(int ,void(*)(int)))(int);//代码2
typedef关键字
typedef是用来类型重命名的,可将复杂类型简单化。
typedef unsigned int unit; //将unsigned重命名为unit typedef int* ptr_t; //将int*重命名为ptr_t
特殊的,对于数组指针类型和函数指针类型
typedef int(*parr_t)[5];//新类型名必须在*的右边。 typedef void(*pfun_t)(int);//同上
代码2简化:
typedef void(*pfun_t)(int); pfun_t signal(int,pfun_t);
对于代码1的理解:
-
假设fp是个float指针,声明:float * fp;
-
把0强制转换成一个float指针(只需要把变量fp去掉即可):(float *) 0;
-
类似地,假设fp是指针为void类型的函数指针,声明:void (*fp)();
-
把0强制转换为该函数指针:(void(*)())0;
-
最后用(void(*)())0代替fp,从而得到调用的用法:((void( *)())0)();
5.函数指针数组
数组是一个存放相同类型数据的存储空间
int * arr[10];//数组中每个元素都是int *
把函数地址存到一个数组中,那这个数组就叫函数指针数组。
//定义如下: int (*parr1[3])();
6.转移表
函数指针数组的用途:转移表。
//计算器的一般实现 #include<stdio.h> int add(int a,int b) { return a+b; } int sub(int a,int b) { return a-b; } int mul(int a,int b) { return a*b; } int div(int a,int b) { return a/b; } int main() { int x,y; int input = 1; int ret = 0; do { printf("*************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4.div \n"); printf(" 0:exit \n"); printf("*************\n"); printf("请选择"); scanf("%d",&input); switch(input) { case 1: printf("请输入操作数"); scanf("%d %d",&x,&y); ret = add(x,y); printf("ret = %d\n",ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; } }while(input) return 0; }
使用函数指针数组的实现:
#include<stdio.h> int add(int a,int b) { return a+b; } int sub(int a,int b) { return a-b; } int mul(int a,int b) { return a*b; } int div(int a,int b) { return a/b; } int main() { int x,y; int input = 1; int ret = 0; int(*p[5])(int x,int y) = {0,add,sub,mul,div};//转移表本尊 do { printf("*************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4.div \n"); printf(" 0:exit \n"); printf("*************\n"); printf("请选择"); scanf("%d",&input); if((input<=4&&input>=1)) { printf("请输入操作数"); scanf("%d %d",&x,&y); ret = (*p[input](x,y)); printf("ret = %d\n",ret); } else if(input == 0) { printf("退出计算器\n"); } else { printf("输入不合法\n"); } }while(input); return 0; }
part 4
回调函数
回调函数就是一个通过函数指针调用的函数。
如果把函数指针作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时,被调用函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
//使用回调函数改造前 #include<stdio.h> int add(int a,int b) { return a+b; } int sub(int a,int b) { return a-b; } int mul(int a,int b) { return a*b; } int div(int a,int b) { return a/b; } int main() { int x,y; int input = 1; int ret = 0; do { printf("*************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4.div \n"); printf(" 0:exit \n"); printf("*************\n"); printf("请选择"); scanf("%d",&input); switch(input) { case 1: printf("请输入操作数"); scanf("%d %d",&x,&y); ret = add(x,y); printf("ret = %d\n",ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; } }while(input) return 0; } //使用回调函数后 #include<stdio.h> int add(int a,int b) { return a+b; } int sub(int a,int b) { return a-b; } int mul(int a,int b) { return a*b; } int div(int a,int b) { return a/b; } void calc(int(*pf)(int,int)) { int ret; int x,y; printf("输入操作数:"); scanf("%d %d",&x,&y); ret = pf(x,y); printf("ret = %d\n",ret); } int main() { int input = 1; do { printf("*************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4.div \n"); printf(" 0:exit \n"); printf("*************\n"); printf("请选择"); scanf("%d",&input); switch(input) { case 1: calc(add); break; case 2: calc(sub); break; case 3: calc(mul); break; case 4: calc(div); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; }while(input); return 0; }
2. qsort使用举例
qsort()函数是C语言库函数中的一种排序函数,排序方法为快速排序。其特点是可以排序任意类型的数组元素。
2.1 使用qsort函数排序整型数据
#include<stdio.h> int int-cmp(const void * p1,const void *p2) { return (*(int*)p1 - *(int*)p2); } int main() { int arr[]={1,3,5,7,9,2,4,6,8,0}; int i = 0; qsort(arr,sizeof(arr)/sizeof(arr[0]),sizeof(int),int_cmp); for(i=0;i<sizeof(arr)/sizeof(arr[0]);i++) { printf("%d",arr[i]); } printf("\n"); return 0; }
2.2 使用qsort排序结构数据
struct Stu { char name[20]; int age; }; int cmp_stu_by_age(const void* e1,const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; } int cmp_stu_by_name(const void* e1,const void* e2) { return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name); } void test2()//年龄排序 { struct Stu s[] = {"susan",20},{"susa",30},{"sus",15}; int sz = sizeof(s)/sizeof(s[0]); qsort(s,sz,sizeof(s[0]),int cmp_stu_by_age); } void test3() { struct Stu s[] = {"susan",20},{"susa",30},{"sus",15}; int sz = sizeof(s)/sizeof(s[0]); qsort(s,sz,sizeof(s[0]),int cmp_stu_by_name); } int main() { test2(); test3(); return 0; }
2.3 qsort函数的模拟实现
使用回调函数,模拟实现qsort(采用冒泡方式)
#include<stdio.h> int int_cmp(const void * p1,const void *p2) { return (*(int*)p1 - *(int *)p2); } void _swap(void *p1,void *p2,int size) { int i = 0; for(i=0;i<size;i++) { char tmp = *((char *)p1 + i); *((char *)p1 + i) = *((char *)p2 + i); *((char *)p2 + i) = tmp; } } void bubble(void *base, int count, int size, int (*cmp)(void *,void *)) { int i = 0; int j = 0; for(i=0;i<count-1;i++) { for(j=0;j<count-1;j++) { if(com((char *)base + j*size,(char *)base + (j+1)*size)>0) { _swap((char *)base + j*size,(char *)base + (j+1)*size,size); } } } } int main() { int arr[] = {1,3,5,7,9,2,4,6,8,0}; int i = 0; bubble(arr,sizeof(arr)/sizeof(arr[0]),sizeof(int),int_cmp); for(i=0;i<sizeof(arr)/sizeof(arr[0]),i++); { printf("%d",arr[i]); } printf("\n"); return 0; }
part 5
1.sizeof和sizelen的对比
sizeof计算变量所占内存空间大小,单位是字节,如果操作数为类型,计算的则是类型创建变量所占内存空间大小。
【sizeof只关注内存空间大小,不在乎内存中存放什么数据】
#include<stdio.h> int main() { int a = 10; printf("%d\n",sizeof(a)); printf("%d\n",sizeof a);//与上面语句输出相同,区别在于第一个是调用sizeof函数,a是传参,第二个式子是操作符加操作数的模式。 printf("%d\n",sizeof(int)); return 0; }
strlen是C语言库函数,功能是求字符串长度
size_t_strlen(const char * str);
统计的是从strlen函数的参数str这个地址开始向后,\0之前字符串中字符的个数。【strlen函数会一直向后寻找\0(坚定不移),直到找到为止,所以可能存在越界查找】
#include<stdio.h> int main() { char arr1[3] = {'a','b','c'}; char arr[] = "abc"; printf("%d\n",strlen(arr1)); printf("%d\n",strlen(arr2)); printf("%d\n",sizeof(arr1)); printf("%d\n",sizeof(arr2)); return 0; }
对比
sizeof | strlen |
---|---|
1.sizeof是操作符 | 1.strlen是库函数,使用需要包含头文件string.h |
2.sizeof计算操作数所占内存的大小单位是字节 | 2.strlen是求字符串长度的,统计的是\0之前字符的字数 |
3.不关注内存中存放什么数据 | 3.关注内存中是否有\0,如果没有就会持续向后寻找,可能会溢出 |
2.数组和指针笔试试题解析
2.1 一维数组
int a[] = {1,2,3,4}; printf("%d\n",sizeof(a));// 4*4=16 printf("%d\n",sizeof(a+0));// 4或8 printf("%d\n",sizeof(*a));// 4 printf("%d\n",sizeof(a+1));// 4或8 printf("%d\n",sizeof(a[1]));// 4 printf("%d\n",sizeof(&a));// 4或8 printf("%d\n",sizeof(*&a));//16 printf("%d\n",sizeof(&a+1));// 4或8 printf("%d\n",sizeof(&a[0]));// 4或8 printf("%d\n",sizeof(&a[0]+1));// 4或8
-
sizeof(数组名)表示整个数组的大小,即所有元素占内存中的字节大小;
-
a表示首元素地址,a+0还是首元素地址,地址大小为4或8字节;
-
*a表示首元素,整型一维数组中每个元素都是4字节;
-
a+1表示跳过首元素指向第二个元素地址,大小为4或8字节;
-
a[1]表示第二个元素,整型一维数组中的每个元素都是4字节;
-
&a是整个数组的地址,地址大小为4或8;
-
*和&互为逆运算,sizeof( *&a)就是sizeof(a);
-
&a是整个数组的地址,&a+1跳过整个数组,得到的是地址,大小为4或8;
-
&a[0]表示首元素地址,大小为4或8;
-
&a[0]+1表示跳过首元素得到第二个元素地址,大小为4或8.
2.2 字符数组
代码1:
char arr[] = {'a','b','c','d','e','f'}; printf("%d\n", sizeof(arr));//整个字符数组 1*6=6 printf("%d\n", sizeof(arr+0));//数组首元素地址 4 printf("%d\n", sizeof(*arr));//数组首元素 1 printf("%d\n", sizeof(arr[1]));//第二个元素 1 printf("%d\n", sizeof(&arr));//整个数组的地址 4 printf("%d\n", sizeof(&arr+1));//跳过整个数组 4 printf("%d\n", sizeof(&arr[0]+1));//数组第二个元素的地址 4
代码2:
char arr[] = {'a','b','c','d','e','f'}; printf("%d\n", strlen(arr));//数组中元素的个数,但是没有“\0”,所以输出随机值 printf("%d\n", strlen(arr+0));//a的地址,随机值 printf("%d\n", strlen(*arr));//出错 printf("%d\n", strlen(arr[1]));//出错 printf("%d\n", strlen(&arr));//数组地址,随机值 printf("%d\n", strlen(&arr+1));//数组后面的元素的地址,随机值 printf("%d\n", strlen(&arr[0]+1));//第二个元素地址,随机值
代码3:
char arr[] = "abcdef"; printf("%d\n", sizeof(arr));//数组大小,包含“\0” 7 printf("%d\n", sizeof(arr+0));//a的地址 4 printf("%d\n", sizeof(*arr));//1 printf("%d\n", sizeof(arr[1]));//1 printf("%d\n", sizeof(&arr));//数组地址长度 4 printf("%d\n", sizeof(&arr+1));//4 printf("%d\n", sizeof(&arr[0]+1));//4
代码4:
char arr[] = "abcdef"; printf("%d\n", strlen(arr));//字符串长度 6 printf("%d\n", strlen(arr+0));//6 printf("%d\n", strlen(*arr));//出错 printf("%d\n", strlen(arr[1]));//出错 printf("%d\n", strlen(&arr));//6 printf("%d\n", strlen(&arr+1));//随机值 printf("%d\n", strlen(&arr[0]+1));//第一个元素后面的字符串长度 5
代码5:
char *p = "abcdef"; printf("%d\n", sizeof(p));//4 printf("%d\n", sizeof(p+1));//4 printf("%d\n", sizeof(*p));//1 printf("%d\n", sizeof(p[0]));//1 printf("%d\n", sizeof(&p));//4 printf("%d\n", sizeof(&p+1));//4 printf("%d\n", sizeof(&p[0]+1));//4
代码6:
char *p = "abcdef"; printf("%d\n", strlen(p));//6 printf("%d\n", strlen(p+1));//5 printf("%d\n", strlen(*p));//出错 printf("%d\n", strlen(p[0]));//出错 printf("%d\n", strlen(&p));//随机值 printf("%d\n", strlen(&p+1));//随机值 printf("%d\n", strlen(&p[0]+1));//5
2.3 二维数组
int a[3][4] = {0}; printf("%d\n",sizeof(a));//数组大小 48 printf("%d\n",sizeof(a[0][0]));//首元素 4 printf("%d\n",sizeof(a[0]));//数组首行 16 printf("%d\n",sizeof(a[0]+1));//第一行第二个元素地址 4 printf("%d\n",sizeof(*(a[0]+1)));//第一行第二个元素 4 printf("%d\n",sizeof(a+1));//数组第二行地址 4 printf("%d\n",sizeof(*(a+1)));//数组第二行 16 printf("%d\n",sizeof(&a[0]+1));//数组第二行地址 4 printf("%d\n",sizeof(*(&a[0]+1)));//数组第二行 16 printf("%d\n",sizeof(*a));//数组第一行 16 printf("%d\n",sizeof(a[3]));//数组第三行下一行的字节大小 16
数组名的意义:
-
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小;
-
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址;
-
除此之外所有的数组名都表示首元素的地址。
3.指针运算笔试试题解析
3.1 题目一
#include <stdio.h> int main() { int a[5] = { 1, 2, 3, 4, 5 }; int *ptr = (int *)(&a + 1); printf( "%d,%d", *(a + 1), *(ptr - 1)); return 0; }
&a代表整个数组的地址,&a+1就是跳过该数组,指向下一个数组地址,(int *)是强制类型转换,将数组指针转换成整型指针,可知ptr-1指向“5”,a+1指向第二个元素“2”
本题输出:2 5
3.2 题目二
struct Test {int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; }*p = (struct Test*)0x100000; int main() { printf("%p\n", p + 0x1);//0x1是16进制的1,%p是以16进制输出。 printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; } //在X86环境下 //假设结构体的⼤⼩是20个字节 //程序输出的结果是啥?
p地址是0X100000,结构体大小为20字节,p是结构体指针,p+1就是加上其所指类型的大小即一个结构体的大小(20字节),第一个输出为:0X100014(16进制);
(unsigned long)将结构体指针转换成无符号长整型类型的指针,p+1就相当于整数加1,第二个输出:0X100001;
(unsigned int*)将结构体指针转换成无符号整型指针类型,相当于整型指针+1,第三个输出:0X100004
3.3 题目三
#include <stdio.h> int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; int *p; p = a[0]; printf( "%d", p[0]); return 0; }
(数字,数字)是逗号表达式,所以二维数组中实际存储的值是{1,3,5};p指向第一行的一维数组{1,3},p[0]值为1,最终输出为:1
3.4 题目四
//假设环境是x86环境,程序输出的结果是啥? #include <stdio.h> int main() { int a[5][5]; int(*p)[4]; p = a; printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); return 0; }
此题中p是一个每行4个元素的二维数组指针,第一个表达式是 &p - &a
,计算的是 p
指向的二维数组中下标为 (4, 2) 的元素(实际上是a[4] [3])地址与 a
数组中下标为 (4, 2) 的元素(a[5] [2])地址之间的差值。第二个表达式是 &p - &a
,计算的是 p
指向的二维数组中下标为 (4, 2) 的元素地址与 a
数组中下标为 (4, 2) 的元素地址之间的差值。
输出结果是:F F F F F F F C(十六进制) -4(十进制)
3.5 题目五
#include <stdio.h> int main() { int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *ptr1 = (int *)(&aa + 1); int *ptr2 = (int *)(*(aa + 1)); printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1)); return 0; }
&aa是数组的地址,&aa+1是加上数组的大小,之后又将int(*)[]类型转换为int *,所以 *(ptr1-1)就为10;aa是首元素地址,+1为第二个元素地址,( *(aa+1))就是第二个一维数组{6,7,8,9,10},之后将int( *)[]类型转换为int *,所以 *(ptr2-1)为5.
输出结果:10 5
3.6 题目六
#include <stdio.h> int main() { char *a[] = {"work","at","alibaba"}; char**pa = a; pa++; printf("%s\n", *pa); return 0; }
输出结果为:at
3.7 题目七
#include <stdio.h> int main() { char *c[] = {"ENTER","NEW","POINT","FIRST"}; char**cp[] = {c+3,c+2,c+1,c}; char***cpp = cp; printf("%s\n", **++cpp); printf("%s\n", *--*++cpp+3); printf("%s\n", *cpp[-2]+3); printf("%s\n", cpp[-1][-1]+1); return 0; }
输出结果:POINT ER ST EW
分析:
-
++cpp
会将cpp
指向下一个指针数组元素。**
用于解引用两次,即获取指向字符常量字符串的指针。所以这里将会打印POINT
。 -
++cpp
将cpp
移动到下一个指针数组元素(此操作在上一步的基础上,所以此时实际上移动了两次),*
解引用一次,得到一个指向字符指针的指针,然后--
将其再移回到前一个元素,再解引用一次,得到一个指向字符常量字符串的指针。+3
将指针移动到字符串的第四个字符处。所以这里将会打印ER
. -
注意此时cpp已经++两次,下面操作都是在此基础上进行计算。
-
cpp[-2]等价于*(cpp-2),即cpp[-2]得到的是cpp[0]。
-
cpp[-1] [-1]+1,等价于*( * (cpp-1)-1)+1,首先cpp-1解引用得到cp[1]的内容c+2,对其-1则为c+1,对c+1解引用得到指向NEW中N的一级指针char*,再对该一级指针+1,便指向了E,所以最后以%s打印时得到的结果为EW。