第一章:字符指针
int main()
{
char ch = 'w';
char* pc = &ch;//*说明pc是指针,char说明pc指向的对象ch类型是char。char*是pc的类型
*pc = 'b';//通过指针找到ch修改里面的值
printf("%c\n", ch);
char* p = "abcdef";//这里是把字符串首字母a的地址赋值给指针p
printf("%s\n", p);
//上面写法在某些编译器会报警告不安全。因为abcdef是常量字符串,常量字符串的意思就是不能被修改。
//但是又把这个字符串的地址放到p里面,p的权限就变大了(因为p没有被修饰)。如果强行修改,程序就崩溃。
//所以应该在要在*左边加上const,这样const就修饰指针*p,也就是限制了*p不能修改
char arr[] = "abcdef";//这种写法才是把整个字符串放到数组里
return 0;
}
练习题
int main()
{
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
//内存中有一个字符串abcdef\0,这是一个常量字符串,放在内存只读数据区里(不能改)。
//因为不能改,所以只需要存一份。所以p1和p2指向的是同一个地址
if (p1 == p2)
printf("p1 == p2\n");
else
printf("p1 != p2\n");
//arr1和arr2是两个独立的数组,在内存中开辟了两块独立的空间,所以地址不同
if (arr1 == arr2)
printf("arr1 == arr2\n");
else
printf("arr1 != arr2\n");
return 0;
}
第二章:指针数组
1. 定义
指针数组 - 是数组,是用来存放指针的数组。
int main()
{
int* arr1[10];//存放整型指针的数组,数组每个元素的类型是int*(即int*类型的值)
char* arr2[4];//存放字符指针的数组
char* arr3[5];//一级字符指针的数组
char** arr3[5];//二级字符指针的数组
return 0;
}
2. 用指针数组模拟二维数组
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;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
//*(p+i)等价于p[i]
printf("%d ", *(parr[i] + j));//起始地址加j,相当于向后平移j个元素
//printf("%d ", parr[i][j]);//等价上方
}
printf("\n");
}
return 0;
}
第三章:数组指针
1. 定义
数组指针 - 是指针,指向数组的指针
int main()
{
//p1先跟[]结合,是个数组,有10个元素,p1是数组名,数组元素类型是int*。p1是指针数组
int* p1[10];
//p2先跟*结合,p2是指针,指向的是数组,数组元素10个,数组元素类型是int。p2是数组指针。
//p2可以指向一个数组,该数组有十个元素,每个元素是int类型
int(*p2)[10];
//解释:p2先和*结合,说明p2是一个指针变量,然后指针指向的是一个大小为10个整型的数组。
//所以p2是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p2先和*结合。
return 0;
}
2. &数组名 VS 数组名
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
//上方两个打印结果一样,说明数组名就是数组首元素地址
printf("%p\n", arr);//012FF740
printf("%p\n", arr + 1);//012FF744
printf("%p\n", &arr[0]);//012FF740
printf("%p\n", &arr[0] + 1);//012FF744
printf("%p\n", &arr);//012FF740
printf("%p\n", &arr + 1);//012FF768,增加了十六进制的28,也就是十进制的40字节
int sz = sizeof(arr);
printf("%d\n", sz);//40
//数组名通常表示的都是数组首元素的地址
//但是有2个例外
//1. sizeof(数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
//2. &数组名,这里的数组名表示的依然是整个数组,所以&数组名取出的是整个数组的大小
//整形指针是用来存放整形的地址
//字符指针是用来存放字符的地址
//数组指针是用来存放数组的地址(即&arr就是取出整个数组的地址)
int arr[10] = { 0 };
int* p = arr;//将数组首元素地址存到p
int(*p2)[10] = &arr;//*p2是指针,需要使用括号,指向一个有10个元素的数组(即[10]),每个元素的类型是int
return 0;
}
3. 数组指针的使用
指向指针数组的指针
int main()
{
char* arr[5] = { 0 };
char* (*pc)[5] = &arr;
return 0;
}
错误写法
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[] = &arr;//错误写法,元素个数不能省略
int(*p)[10] = &arr;//正确写法
return 0;
}
不推荐用法
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
//p是指向数组的(p存放的是整个数组的地址),*p的意思是通过数组地址找到数组
//*p(对p解引用)其实就相当于数组名,数组名又是数组首元素的地址。
//所以*p本质上是数组首元素的地址
//int a = 10; int* p = &a; *p->a
printf("%d ", *(*p + i));//此方法不合理,不推荐
}
//正常写法
int* p = arr;//数组名是首元素地址,赋给整形指针p
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
常见用法
void print1(int arr[3][5], int r, int c)//数组形式
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print2(int(*p)[5], int r, int c)//指针形式
{
//p指向的是二维数组的行,p加几就跳过几行。p+i是第i行的地址,对p+i解引用就是拿到第i行。
//因为*(p+i) <=> *(arr+i) <=> arr[i] <=> p[i]
//所以*p相当于每行的数组名(每一行是一个一维数组),而每行的数组名又相当于首元素地址。
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));
//printf("%d ", p[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 };
//print1(arr, 3, 5);
print2(arr, 3, 5);
//int arr[3][5];
//arr数组名表示数组首元素地址
//二维数组首元素是第一行
//此时arr表示数组第一行的地址
//第一行是5个整型,每个元素为int的数组
//所以arr是5个元素的整型数组的地址
//总结:二维数组传参传的是首元素(二维数组的第一行)的地址,第一行是一个一维数组,按照一维数组的数组指针方式写即可
return 0;
}
看看下面代码的意思:
int main()
{
int arr[5];//整型数组
int* parr1[10];//整形指针数组
int(*parr2)[10];//数组指针
int(*parr3[10])[5];//parr3先和[10]结合,它是数组。
//parr3[10]拿走就剩下int(*)[5](这是一个数组指针)。所以parr3是存放数组指针的数组
//它声明了一个长度为10的数组 parr3,其中每个元素都是一个指向长度为5的一维数组的指针。
//int(*parr3[10]) 表示一个长度为10的数组,每个元素都是一个指向 int[5] 类型的指针。
//也就是说,parr3 是一个指针数组,它可以存储指向长度为5的一维数组的指针。
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int(*parr3[10])[5] = { &arr1,&arr2,&arr3 };
//[5]表示每个元素都是一个指向长度为5的一维数组的指针,parr3[10]表示这个数组有10个元素
//parr3[10]是一个数组,
//*parr3[10]是一个指针数组,有10个元素
//int(*parr3[10])[5],每个元素是指针且指向一个有5个元素的数组(即数组指针),且该数组类型为int。所以parr3是存放数组指针的数组
return 0;
}
第四章:数组参数、指针参数
1. 一维数组传参
void test(int arr[])//参数写成数组形式。可行,元素个数可以不写
{}
void test(int arr[10])//可行
{}
void test(int* arr)//可行,数组名是首元素地址,首元素是int类型,所以可以用指针接收
{}
void test2(int* arr[20])//可行,用数组形式接收
{}
void test2(int** arr)//可行,因为首元素地址是int*的地址,所以用二级指针来存放一级指针变量的地址
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };//arr2有20个元素,每个元素是int*
//arr2是数组名,也是首元素地址,首元素是int*的地址
test(arr);
test2(arr2);
}
2. 二维数组传参
void test(int arr[3][5])//可行,用数组形式接收
{}
void test(int arr[][])//不可行,形参的二维数组,行可以省略,列不能省略
{}
void test(int arr[][5])//可行,省略了行,但没省略列
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//不可行,二维数组的数组名,表示首元素的地址,其实是第一行的地址,所以一维数组的地址不能放进一级指针
{}
void test(int* arr[5])//不可行,因为这里不是指针形式。这里是指针数组
{}
void test(int(*arr)[5])//可行,*arr是指针,指向的是5个元素,每个元素是int类型的数组
{}
void test(int** arr)//不可行,首元素地址是一个一维数组的地址,不能用二级指针接收
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
3. 一级指针传参
void print(int* p, int sz)//指针传参,实参是什么指针,形参就用什么指针接收
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;//arr是数组名,也是首元素的地址(即1的地址,是个整形)
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
//如果函数的参数部分是指针,传参可以传什么
void print2(int* p)
{}
int main()
{
int a = 10;
int* ptr = &a;
print2(&a);//如果函数的参数部分是指针,可以传地址
print2(ptr);//可以传指针变量
int arr[10];
print2(arr);//可以传数组名
return 0;
}
4. 二级指针传参
void test(int** ptr)//传的是二级指针,所以用二级指针接收
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);//一级指针的地址传参,也可以用二级指针接收
return 0;
}
//如果函数的形式参数是二级指针,调用函数的时候可以传什么实参
test(int** p)
{}
int main()
{
int* p1;
int** p2;
int* arr[10];//指针数组
test(&p1);
test(p2);
test(arr);//数组名是首元素地址,首元素是int*类型(即一级指针),所以一级指针的地址要用二级指针来接收
return 0;
}
第五章:函数指针
1. 定义
函数指针 - 可以跟数组指针类比。数组指针是指向数组的指针。
函数指针就是指向函数的指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
//&函数名 - 取出的是函数地址
printf("%p\n", &Add);//函数名和&函数名打印的地址一样
printf("%p\n", Add);
//对于函数来说,&函数名和函数名都是函数的地址
int(*pf)(int, int) = &Add;
//*pf是指针,
//最后一个()说明该指针指向函数,指向的函数的参数类型是(int, int)
//第一个int是函数返回的类型
return 0;
}
2. 函数指针有什么用
int Add(int x, int y)
{
return x + y;
}
int main()
{
//通过整形指针来理解
int a = 10;
int* pa = &a;
*pa = 20;
printf("%d\n", *pa);
//指针变量存了地址以后,可以通过地址找到该对象访问或修改
//所以函数指针也是一样
int(*pf)(int, int) = &Add;
int ret = *(pf)(2, 3);
printf("%d\n", ret);
return 0;
}
3. 函数指针的使用
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(2, 3);//直接调用
int(*pf)(int, int) = &Add;
int ret = (*pf)(2, 3);//通过函数地址间接调用
//(*pf)相当于对函数指针解引用找到函数,然后调用函数
//调用函数需要传参,(2, 3)是传参
int ret = pf(2, 3);
//第二种写法:可以省略*。
//如果要加*必须放在括号里,即(*pf)。
//但这里的*无实际作用,只是为了可读性,即pf是个指针。但带*是标准写法
//为什么可以不写*
//因为&Add和Add都可以表示函数的地址。
//Add可以赋给pf,Add又可以直接调用函数,所以pf同样可以(因为pf保存也是函数地址)
int(*pf)(int, int) = &Add;
int(*pf)(int, int) = Add;
int ret = Add(2, 3);
int ret = pf(2, 3);
printf("%d\n", ret);
return 0;
}
4. 函数指针作用的示例
int Add(int x, int y)
{
return x + y;
}
void calc(int(*pf)(int, int))//因为接收的是函数的地址,所以形参应该是函数指针
{
int a = 3;
int b = 5;
int ret = pf(a, b);//通过函数指针调用Add函数
printf("%d\n", ret);
}
int main()
{
calc(Add);//函数名传参,函数名就是函数地址
return 0;
}
5. 函数指针代码
代码1
int main()
{
//代码1
//以下代码是一次函数的调用,调用的是0作为地址处的函数。
//1. 把0强制类型准换为:无参,返回类型是void的函数地址
//2. 调用0地址处的这个函数
( *( void (*)() )0 )();
//void(*p)();//p是函数指针
//void(*)();//是一种函数指针类型
//(void(*)())0;//0是整数,但前面是函数指针类型,说明要强制类型转换(将整形转换为函数指针类型),而函数指针类型存的是函数地址,所以可以认为0是个地址
//这里是把0强制类型转换成函数指针类型,这时候(void(*)())0就是一个函数的地址
//也就是0地址放一个函数,函数没有参数,返回类型为void
//(*(void (*)())0);//这里是用第一颗*解引用找到函数
//(*(void (*)())0)();//又因为这个函数没有参数,所以传参时什么都不传
//它执行了一个函数指针的调用。代码中的(void (*)()) 表示一个函数指针类型,0 表示空指针。
//整个表达式(*(void (*)())0)() 将空指针转换为函数指针,并调用该函数。
return 0;
}
代码2
typedef unsigned int unit;//通过此方法重命名函数指针
typedef void(*)(int) pf_t;//此写法错误,要将pf_t放到*旁边
typedef void(*pf_t)(int);//把void(*)(int)类型重命名为pf_t。pf_t是一个类型
int main()
{
//以下代码是一次函数声明,signal是函数名,。
//声明的函数的第一个参数类型是int,
//第二个参数类型是函数指针(该函数指针指向的函数参数是int,返回类型是void)。
//signal函数的返回类型是一个函数指针(该函数指针指向的函数参数是int,返回类型是void)
void ( *signal( int, void(*)(int) ) )(int);
//signal先和后面的括号结合,所以是函数名
//signal(int, void(*)(int));//后面的int和void(*)(int)是参数类型。
//第二个类型是函数指针。这里是函数的声明,只需要写函数返回类型,函数名和参数类型
//把上方代码去掉,还剩void(*)(int)这是一个函数指针类型,说明signal函数的返回类型是函数指针类型
//上方代码不好理解,需要简化,通过typedef重命名
pf_t signal(int, pf_t);//signal函数的参数第一个是int,第二个是函数指针类型,返回类型是函数指针类型
return 0;
}
第六章:函数指针数组
1. 定义
把函数指针放在数组中,就是函数指针的数组
2. 函数指针数组用途
计算器示例
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;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
//int x = 0;
//int y = 0;
//int ret = 0;
//输入操作数代码放在这里会有问题
//如果选择的不是1234,依然要输入操作数,这不符合逻辑,所以要放进switch里
switch (input)
{
case 1:
printf("请输入两个操作数:>");//但是放进switch里有新问题,代码冗余。case1234中只有一句代码是不重复的
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
使用函数指针数组的实现:
//假设还想实现x&y,x^y,x|y,x&&y,x||y,x>>y,x<<y等功能
//需要在菜单函数增加选项,并增加对应的计算函数,还有主函数中的case选项也要增加
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 x = 0;
int y = 0;
int ret = 0;
int input = 0;
//使用函数指针数组,可以轻易的增加或删除想要的计算功能 (转移表)
int(*pfArr[])(int, int) = { 0,Add,Sub,Mul,Div };//0占位使用,为了让Add和下标1对齐
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);//input是几就访问这个数组下标为几的元素
printf("%d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
第七章:指向函数指针数组的指针
指向函数指针数组的指针是一个
指针,
指针指向一个
数组
,数组的元素都是
函数指针
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(*pf)(int, int) = Add;
//函数指针数组
int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
//指向【函数指针数组】的指针
int(*(*ppfArr)[5])(int, int) = &pfArr;
//pfArr先和*结合,说明是指针。
//该指针指向的是[5],说明指向了数组
//数组的元素类型是int(*)(int, int),即函数指针
return 0;
}
第八章:回调函数
1. 定义
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
2. 计算器(回调函数版本)
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;
}
void calc(int(*pf)(int, int))//回调函数
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
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;
}
3. 复习冒泡排序
下方函数只能排序整型数组
//假设数组是0,1,2,3,4,5,6,7,8,9 该函数还是要两两比较,只是没有交换。
//并且还是要遍历9轮。但是如果第一轮遍历完发现一个都没有交换,说明这个数组已经是排序好了。
void bubble_sort(int arr[], int sz)
{
int i = 0;//排序的轮数
for (i = 0; i < sz - 1; i++)//总共进行元素个数sz-1轮排序
{
int j = 0;//元素下标
for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
优化版本
void bubble_sort(int arr[], int sz)
{
int i = 0;//排序的轮数
for (i = 0; i < sz - 1; i++)//总共进行元素个数sz-1轮排序
{
int flag = 1;//假设数组是拍好序的
int j = 0;//元素下标
for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;//一旦有元素交换了,就要把flag置成0
}
}
if (flag == 1)//如果flag还等于1,说明数组已经有序
{
break;
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//把数组排成升序
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;
}
4. 演示qsort函数使用
#include <stdlib.h>
//使用库函数qsort - 可以排序任意类型的数据
void qsort(void* base, //要排序的数据的起始位置
size_t num, //待排序的数据元素个数
size_t width,//待排序数据元素的大小(单位是字节)
//因为不同数据比较方法不同,不仅限于>或<之类。所以要把比较方法提取出来
//e1是要比较的第1个元素,e2是要比较的第2个元素,e1和e2是要比较元素的地址
int(__cdecl* cmpare)(const void* elem1, const void* elem2)//函数指针-比较函数
);
__cdecl - 函数调用约定,按c的方式去调用,可以去掉
//比较两个整形元素
//e1指向一个整数,e2指向另外一个整数
//e1和e2都是指针,存的都是地址
int cmp_int(const void* e1, const void* e2)
{
//void*指针是不能直接解引用
//void*指针的作用
//int a = 10;
//char* pa = &a;//类型不匹配,编译器会报警告
//void* pv = &a;//void*是无具体类型的指针,可以接收任意类型的地址
//void* 是无具体类型的指针,所以不能解引用操作,也不能+-整数操作
if (*(int*)e1 > *(int*)e2)
return 1;
else if (*(int*)e1 == *(int*)e2)
return 0;
else
return -1;
//等效上方
return (*(int*)e1 - *(int*)e2);//升序
//return (*(int*)e2 - *(int*)e1);//降序
<0 The element pointed to by e1 goes before the element pointed to by e2
0 The element pointed to by e1 is equivalent to the element pointed to by e2
>0 The element pointed to by e1 goes after the element pointed to by e2
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//把数组排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);//最后一个参数是把函数传给函数指针
//打印排序好的数组
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
5. 使用qsort排序结构体
struct Stu
{
char name[20];
int age;
};
#include <string.h>
int cmp_stu_by_name(const void* e1, const void* e2)//比较名字
{
//不能直接使用e1,因为它是void*类型,所以需要强制类型转换成结构体指针
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//两个名字是两个字符串,所以使用strcmp函数
//strcmp函数返回值是大于0 等于0 小于0的数字
}
int cmp_stu_by_age(const void* e1, const void* e2)//比较年龄
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//两个名字是两个字符串,所以使用strcmp函数
}
void test2()
{
//测试使用qsort来排序结构体
struct Stu s[] = { {"zhangsan", 18},{"lisi", 30},{"wangwu", 25} };
int sz = sizeof(s) / sizeof(s[0]);
//qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);//名字字母升序
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);//年龄升序
}
int main()
{
test2();
6. 使用回调函数,模拟实现qsort(采用冒泡的方式)
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);//升序
//return (*(int*)e2 - *(int*)e1);//降序
}
//参考qsort函数重新设计冒泡排序
void Swap(char* buf1, char* buf2, int width)
{
//该函数是把buf1指向位置和buf2指向位置 向后width宽度字节的内容交换
int i = 0;
//width是数据的宽度,也是一个数据所占字节
for (i = 0; i < width; i++)//这里一次交换一个字节,如果是int,width=4,交换4次
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
int i = 0;//排序的轮数
for (i = 0; i < sz - 1; i++)//总共进行元素个数sz-1轮排序
{
int flag = 1;//假设数组是拍好序的
int j = 0;//元素下标
for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=0)
{
//base是void*,void* 是无具体类型的指针,所以不能解引用操作,也不能+-整数操作
//所以要强制类型转换。如果用int*一次跳过太多字节,所以用char*提高精度
//这里用下标j来乘以宽度就可以决定一次跳过几个字节
//大于0说明前一个数比后一个数大,不符合升序,要交换
//这里是调用比较函数,传的参数是代比较两个元素的地址
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//交换
//光有起始位置还不够,还需要知道宽度,即多少字节
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;//只要产生交换就要将flag置成0,避免下方跳出循环
}
}
if (flag == 1)//如果flag还等于1,说明数组已经有序
{
break;
}
}
}
void test3()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
//把数组排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//最后一个参数是把函数传给函数指针
//打印排序好的数组
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
//使用自定义函数bubble_sort(模拟qsort)来排序结构体
int cmp_stu_by_name(const void* e1, const void* e2)//比较名字
{
//不能直接使用e1,因为它是void*类型,所以需要强制类型转换成结构体指针
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//两个名字是两个字符串,所以使用strcmp函数
//strcmp函数返回值是大于0 等于0 小于0的数字
}
int cmp_stu_by_age(const void* e1, const void* e2)//比较年龄
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//两个名字是两个字符串,所以使用strcmp函数
}
void test4()
{
struct Stu s[] = { {"zhangsan", 18},{"lisi", 30},{"wangwu", 25} };
int sz = sizeof(s) / sizeof(s[0]);
//bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);//年龄升序
bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);//名字升序
}
int main()
{
test3();
test4();
return 0;
}
第九章:指针和数组笔试题解析
一维数组
int main()
{
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//16
//sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节
printf("%d\n", sizeof(a + 0));// 4/8
//a不是单独放在sizeof内部,也没有取地址,所以a就是首元素地址,a+0还是首元素地址
//是地址,大小就是4/8个字节。a <==> &a[0],a+0 <==> &a[0]+0
printf("%d\n", sizeof(*a));// 4
//a是首元素地址,对首元素地址解引用得到首元素,*a=1,sizeof(1)表示1个整型大小
//a <==> &a[0],*a <==> *&a[0],*&a[0] <==> a[0]
printf("%d\n", sizeof(a + 1));//4
//a+1指向第二个元素的地址
printf("%d\n", sizeof(a[1]));//4
//计算的是第二个元素的所占内存空间的大小
printf("%d\n", sizeof(&a));// 4/8
//&a取出的是整个数组的地址
printf("%d\n", sizeof(*&a));//16
//&a(int (*)[4])取出整个数组的地址,数组指针解引用就访问整个数组,等价于sizeof(a)
//或者理解为&和*抵消
printf("%d\n", sizeof(&a + 1));// 4/8
//&a取出整个数组的地址,+1跳过整个数组,但依然是地址
//&a+1 是从数组a的地址向后跳过了一个(4个整型元素)数组的大小
printf("%d\n", sizeof(&a[0]));// 4/8
//&a[0]是第一个元素的地址,计算地址的大小
printf("%d\n", sizeof(&a[0] + 1));// 4/8
//计算第二个元素地址的大小
return 0;
}
字符数组1
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//6
//数组名单独放在sizeof中,表示整个数组,计算的是整个数组的大小
printf("%d\n", sizeof(arr + 0));// 4/8
//arr+0是数组首元素地址
printf("%d\n", sizeof(*arr));// 1
//*arr是数组首元素,大小是1字节。*arr <==> *(arr+0) <==> arr[0]
printf("%d\n", sizeof(arr[1]));// 1
//计算第二个元素的大小
printf("%d\n", sizeof(&arr));// 4/8
//&arr取出整个数组的地址
printf("%d\n", sizeof(&arr + 1));// 4/8
//&arr取出整个数组的地址,+1跳过整个数组的地址
printf("%d\n", sizeof(&arr[0] + 1));// 4/8
//第二个元素的地址
//strlen
//size_t strlen(const char* string);
//strlen参数是指针,所以传过去的是地址
printf("%d\n", strlen(arr));//随机值(>=6)
//数组里没有\0,要找到/0才停下来
printf("%d\n", strlen(arr + 0));//随机值
//arr是首元素地址,+0还是首元素地址
printf("%d\n", strlen(*arr));//野指针
//arr是首元素地址,首元素地址解引用得到是首元素
//此代码相当于strlen('a'),a的ASCII码值是97,所以又相当于strlen(97),相当于把97作为地址传给strlen
//97这种地址不能随便传,而是空间开辟给你的时候,才是有效的。知道地址还不行,要分配给你才可以访问
//这时97就变成野指针,程序报错
printf("%d\n", strlen(arr[1]));//野指针
//相当于strlen('b'),又相当于strlen(98)。同上方一样问题
printf("%d\n", strlen(&arr));//随机值
//&arr取出整个数组的地址,不过也是从首元素地址开始
//虽然arr和&arr类型不一样,但都是传给strlen,所以都是从首元素开始
printf("%d\n", strlen(&arr + 1));//随机值(随机值-6)
//&arr取出整个数组,+1跳过整个数组,从数组后方开始找/0
printf("%d\n", strlen(&arr[0] + 1));//随机值(随机值-1)
//&arr[0]首元素地址,+1是第二个元素地址,从这开始向后找/0
return 0;
}
字符数组2
//strlen是求字符串长度的,关注的是字符串中的\0,计算的是\0之前出现的字符的个数
//strlen是库函数,只针对字符串
//sizeof只关注占用内存空间大小,不在乎内存中放的是什么
//sizeof是操作符
int main()
{
char arr[] = "abcdef";// [abcdef/0]
printf("%d\n", sizeof(arr));//7
//数组名单独放到sizeof中,表示整个数组,求数组大小
printf("%d\n", sizeof(arr + 0));// 4/8
//arr是数组名,首元素地址,+0还是首元素地址
printf("%d\n", sizeof(*arr));//1
//arr是数组名,解引用得到首元素
printf("%d\n", sizeof(arr[1]));//1
//求第二个元素大小
printf("%d\n", sizeof(&arr));// 4/8
//&arr取出整个数组的地址
printf("%d\n", sizeof(&arr + 1));// 4/8
//&arr取出整个数组地址,+1跳过整个数组,即数组后面的地址
printf("%d\n", sizeof(&arr[0] + 1));// 4/8
//&arr[0]+1 第二个元素的地址,b的地址
printf("%d\n", strlen(arr));//6
//arr是数组名,是首元素地址
printf("%d\n", strlen(arr + 0));//6
//arr是数组名,也是首元素地址,+0还是首元素地址
printf("%d\n", strlen(*arr));//报错
//*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
return 0;
}
字符指针
int main()
{
char* p = "abcdef";//把首字符地址放到p里,内存里存放的是abcdef\0,这是一个常量字符串。p和字符串在内存中各自有自己的独立空间
printf("%d\n", sizeof(p));// 4/8
//p是指针变量
printf("%d\n", sizeof(p + 1));// 4/8
//p+1仍然是地址
printf("%d\n", sizeof(*p));//1
//对char*类型指针解引用,得到一个字符
printf("%d\n", sizeof(p[0]));//1
//p[0] <==> *(p+0)
printf("%d\n", sizeof(&p));// 4/8
//&p是二级指针
printf("%d\n", sizeof(&p + 1));// 4/8
printf("%d\n", sizeof(&p[0] + 1));// 4/8
//p[0]是第一个字符,&取地址就是a的地址,+1是b的地址
printf("%d\n", strlen(p));//6
//p存放的是a的地址,向后找\0
printf("%d\n", strlen(p + 1));//5
printf("%d\n", strlen(*p));//报错
//*p是a
printf("%d\n", strlen(p[0]));//报错
printf("%d\n", strlen(&p));//随机值
//从指针p的位置开始向后找\0
printf("%d\n", strlen(&p + 1));//随机值
printf("%d\n", strlen(&p[0] + 1));//5
//p[0]指向第一个元素,&取地址就是a的地址,+1是b的地址
return 0;
}
二维数组
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//48
//sizeof(数组名),计算整个数组大小
printf("%d\n", sizeof(a[0][0]));//4
//第一行第一列,即第一个元素
printf("%d\n", sizeof(a[0]));//16
//a[0]是第一行数组名,所以sizeof(a[0])就是sizeof(数组名),计算第一行的大小
printf("%d\n", sizeof(a[0] + 1));//4
//这里a[0]没有单独放在sizeof里,表示的是第一行第一个元素的地址,等价于&a[0][0],+1表示第一行第二个元素的地址
printf("%d\n", sizeof(*(a[0] + 1)));//4
//a[0]+1表示第一行第二个元素的地址,解引用就得到该整形元素
printf("%d\n", sizeof(a + 1));// 4/8
//a虽然是二维数组的地址,但是并没有单独放在sizeof内部,也没有取地址
//a表示首元素地址,二维数组的首元素是它的第一行,a就是第一行的地址
//a+1是第二行的地址
printf("%d\n", sizeof(*(a + 1)));//16
//对第二行的地址解引用,得到第二行
//*(a+1) <==> a[1],所以sizeof(*(a+1)) <==> sizeof(a[1])
printf("%d\n", sizeof(&a[0] + 1));// 4/8
//a[0]是第一行数组名,&取地址就取出第一行的地址,+1指向第二行
//所以(a+1) <==> (&a[0]+1)
printf("%d\n", sizeof(*(&a[0] + 1)));//16
//(&a[0] + 1)是第二行的地址,解引用得到第二行
//*(a + 1) <==> *(&a[0] + 1)
printf("%d\n", sizeof(*a));// 16
//a表示首元素地址,二维数组首元素是第一行,对第一行的地址解引用,得到第一行
printf("%d\n", sizeof(a[3]));//16
//a[3]可以理解为第四行数组名,而sizeof(数组名)是计算数组大小
//这里是根据二维数组的类型去分析,不是真的访问,本质上sizeof(a[3])等价于sizeof(a[0])
//int a = 10;
//sizeof(int);//这个直接告诉sizeof 是什么类型
//sizeof(a);//这种是分析a的类型
return 0;
}
总结:
数组名的意义:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。
第十章:指针笔试题
笔试题1
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);//跳过整个数组 后面的地址
printf("%d,%d", *(a + 1), *(ptr - 1));//2 5
//a是数组名,数组名是首元素地址,+1指向第二个元素
//ptr是整形指针,指向数组后面,-1就向前移动一个整形,即数组最后一个元素
return 0;
}
笔试题2
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节(x86)
int main()
{
printf("%p\n", p + 0x1);//0x100014
//p是结构体指针,结构体指针+1跳过一个结构体(即20个字节),而20个字节用十六进制表示是14
printf("%p\n", (unsigned long)p + 0x1);//0x100001
//这里是把十六进制的100000转换成长整型,即十进制的1048576,+1就是1048577,再转换成十六进制就是100001
printf("%p\n", (unsigned int*)p + 0x1);//0x100004
//p被转换成整形指针,+1跳过4个字节,十进制的4用十六进制表示也是4,即100004
return 0;
}
笔试题3
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
//&a取出整个数组的地址,+1跳过数组,再转换成整形指针
int* ptr2 = (int*)((int)a + 1);
//a是数组名,也是首元素地址,将这个值准换成整形
//假设a=0x0012ff40,a+1=0x0012ff44,(int)a+1=0x0012ff41
//将a的地址强制转换成整形后再+1,就是数值+1
//再转换成指针,两个地址相差1,意味着向后移动了1个字节
//又因为vs是小端存储模式,数组a在内存的存储情况如下:
//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
//所以ptr2指向了01后面的00,而ptr是整形指针,向后访问4个字节,就是00 00 00 02
//因为小端存储是倒序存放,所以将内存中的00 00 00 02取出后是02 00 00 00
printf("%x,%x", ptr1[-1], *ptr2);//4 02000000
//prt1[-1] <==> *(prt1+(-1)) <==> *(ptr1-1)
//prt1指向数组后面,向前移动一个整形指向4,4在内存中是小端存储(即倒序存放),即04 00 00 00
//取出来,要倒序,即00 00 00 04,但打印时前面的0省略
return 0;
}
笔试题4
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
//这里是逗号表达式。逗号表达式会从左向右以此计算,整个表达式结果是最后一个表达式的结果
//(0, 1)结果是1;(2, 3)结果是3;(4, 5)结果是5;
//a数组实际结果是{ {1,3},{5,0},{0,0} }
int* p;
p = a[0];
//a[0]是的第一行的数组名,它既没有放到sizeof内部,也没有&取地址,所以数组名表示首元素地址
//即第一行第一个元素1的地址,即a[0][0]的地址,&a[0][0]
printf("%d", p[0]);//1
//p[0] <==> *(p+0)
return 0;
}
笔试题5
int main()
{
int a[5][5];
int(*p)[4];
p = a;
//a是数组名,数组名是首元素地址,二维数组首元素是第一行,即第一行的地址。第一行是5个整型的一维数组
//a的类型是int(*)[5]。p指向二维数组a第一行第一个元素的位置
//p+1或对p解引用一次访问4个整型
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//FFFFFFFC -4
//p[4][2] <==> *(*(p+4)+2)
//p[4][2]相当于a[3][3],两个指针相减得到指针和指针之间的元素个数,即4个
//地址的结果是-4
//10000000000000000000000000000100 - -4原码
//11111111111111111111111111111011 - -4反码
//11111111111111111111111111111100 - -4补码
//把-4的补码当做地址打印ff ff ff fc
//注意:整型地址+1加的是一个整型
return 0;
}
笔试题6
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
//&数组名取出整个数组,+1跳过整个数组,ptr1指向aa数组后面
int* ptr2 = (int*)(*(aa + 1));
//aa是数组名,是首元素地址,二维数组首元素是第一行,aa是第一行地址,+1跳过一行指向第二行
//解引用第二行地址,得到第二行,也相当于拿到第二行数组名,*(aa + 1) <==> aa[1]
//而aa[1]既没有放在sizeof内部,也没有取地址,所以又相当于第二行第一个元素的地址
//在转换成int*,指向第二行第一个元素
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
return 0;
}
笔试题7
int main()
{
char* a[] = { "work","at","alibaba" };
//a是字符指针数组,a存放的是work,at,alibaba首字符的地址
char** pa = a;//等价于char* *pa,第二颗*说明pa是指针,第一颗*说明pa指向的对象是char*类型
//a是数组名,数组名是首元素地址,即work首字符的地址
pa++;//等价于pa+1
//pa指向的是char*,pa+1跳过一个char*
printf("%s\n", *pa);//at
return 0;
}
笔试题8
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
//c是指针数组,c存放的是ENTER,NEW,POINT,FIRST首字符的地址
char** cp[] = { c + 3,c + 2,c + 1,c };
//cp是二级指针数组,存放的是c+3的地址(即FIRST);c + 2的地址(即POINT);c + 1的地址(即NEW);c的地址(即ENTER);
char*** cpp = cp;
//cpp存放的是cp的地址,cp是数组名,即首元素地址,即c+3
printf("%s\n", **++cpp);//point
//++cpp指向了cp数组的第二个元素,即c+2
//第一次解引用得到了cp数组的内容c+2,第二次解引用得到c+2指向的内容point首字符p的地址
printf("%s\n", *-- * ++cpp + 3);//er
//cpp此时指向cp数组的第二个元素,即c+2,再++(即++cpp)就指向第三个元素,即c+1
//*++cpp得到就是c+1,前面的--(即-- * ++cpp)就是让c+1再-1,得到c,而c指向的是enter首字符e的地址
//对上面的地址(*-- * ++cpp)解引用,得到enter,在+3就是跳过3个字符,最后得到er
printf("%s\n", *cpp[-2] + 3);//st
//cpp此时指向cp数组的第三个元素,即c+1(此时c+1已经被改成c)
//cpp-2(等价于* *(cpp-2))指向cp数组第一个元素,解引用得到c+3,再解引用得到first首字符f的地址,在+3就是跳过3个字符,最后得到st
printf("%s\n", cpp[-1][-1] + 1);//ew
//cpp[-1][-1]等价于*(*(cpp-1)-1)
//cpp此时还是指向cp数组的第三个元素,即c+1(此时已经被改成c)
//cpp-1指向cp数组的第二个元素,解引用得到c+2,再-1就是c+1,对c+1解引用得到new首字符n的地址,在+1就是跳过一个字符,最后得到ew
return 0;
}