一、数组
同一类型的变量——元素(element)集中在一起,在内存上排列成一条直线,这就是数组(array)。
1.1 一维数组
一维数组的声明
int arr1[10];
int arr2[2 + 8];
#define N 10
int arr3[N];
int count = 10;
int arr4[count];
// 在C99标准之前,[]中间必须是常量表达式
// C99标准支持了变长数组的概念,数组大小可以是变量
// 变长数组不能初始化
char arr5[10];
float arr6[1];
double arr7[20];
一维数组的初始化。用0对没有赋初始值的元素进行初始化。'/0'的ASCII码值是0。
int arr1[10] = { 1,2,3 }; // 不完全初始化,剩余的元素默认初始化为0
int arr2[10] = { 0 }; // 所有元素都为0
int arr3[] = { 1,2,3,4 }; // 没有指定数组的大小,数组的大小根据初始化的内容来确定
int arr4[5] = { 1,2,3,4,5 };
char arr5[3] = { 'a',98,'c' };
char arr6[10] = { 'a','b','c' }; // a b c /0 /0 /0 /0 /0 /0 /0
char arr7[5] = "abc"; // a b c /0 /0
// 不能通过赋值语句进行初始化,错误写法:
int arr8[3];
arr8 = { 1,2,3 };
C99增加了一个新特性:指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。
// 顺序初始化
int arr[6] = { 0,0,0,0,0,80 };
// 指定初始化
int arr[6] = { [5] = 80 }; // 把arr[5]初始化为80,未初始化的元素都为0
#include <stdio.h>
int main()
{
int arr[10] = { 5,6,[4] = 8,9,7,1,[9] = 3 };
for (int i = 0; i < 10; i++)
{
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
一维数组的使用
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
// 下标从0开始 0 1 2 3 4 5 6 7 8 9
// 计算数组元素的个数
int sz = sizeof(arr) / sizeof(arr[0]);
// 遍历一维数组
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
一维数组在内存中的存储
一维数组在内存中是连续存储的。
1.2 二维数组
以一维数组作为元素的数组是二维数组,以二维数组为元素的数组是三维数组……统称为多维数组。
二维数组的声明
int arr1[3][4]; // [行][列]
char arr2[3][5];
double arr3[2][4];
二维数组的初始化
int arr1[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
// 1 2 3 4
// 2 3 4 5
// 3 4 5 6
int arr2[3][4] = { {1,2},{3,4},{5,6} };
// 1 2 0 0
// 3 4 0 0
// 5 6 0 0
int arr3[][2] = { 1,2,3,4 }; // 二维数组如果有初始化,行数可以省略,列数不能省略
// 1 2
// 3 4
指定初始化器对多维数组也有效。
#include <stdio.h>
int main()
{
int arr[4][4] = { 5,6,[1][3] = 8,9,7,1,[3][2] = 3 };
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
printf("arr[%d][%d] = %d\n", i, j, arr[i][j]);
}
}
return 0;
}
二维数组的使用
#include <stdio.h>
int main()
{
int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
for (int i = 0; i < 3; i++)
{
// 打印一行
for (int j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n"); // 打印一行后换行
}
return 0;
}
// 1 2 3 4
// 2 3 4 5
// 3 4 5 6
二维数组在内存中的存储
二维数组在内存中也是连续存储的。
二维数组X按行顺序存储,其中每个元素占1个存储单元。若X[4][4]的存储地址为0xf8b82140,X[9][9]的存储地址为0xf8b8221c,则X[7][7]的存储地址为?
假设二维数组X有m行n列,第一个元素即X[0][0]的存储地址为start,则:
X[4][4]的存储地址=0xf8b82140=start+4*n*1+4*1 ①
X[9][9]的存储地址=0xf8b8221c=start+9*n*1+9*1 ②
②-①:5n+5=0xdc -> 5n=0xd7=215 -> n=43
X[7][7]的存储地址=start+7*n*1+7*1=(start+4*n*1+4*1)+3*n+3=0xf8b82140+132=0xf8b82140+0x84=0xf8b821c4
1.3 数组名
数组名表示数组首元素的地址,是一个常量指针,不可以改变指针本身的值,没有自增、自减等操作。
数组名和指向数组首元素的指针都可以通过改变偏移量来访问数组中的元素,但数组名是常量指针,指向数组首元素的指针是一般指针。
以下2种情况下数组名表示整个数组:
sizeof(数组名),计算整个数组的大小,单位是字节。
&数组名,取出的是数组的地址。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr); // 0096F7CC 数组首元素的地址
printf("%p\n", arr + 1); // 0096F7D0 指针+1跳过4个字节
printf("%p\n", &arr[0]); // 0096F7CC 数组首元素的地址
printf("%p\n", &arr[0] + 1); // 0096F7D0 指针+1跳过4个字节
printf("%p\n", &arr); // 0096F7CC 数组的地址
printf("%p\n", &arr + 1); // 0096F7F4 指针+1跳过整个数组的大小(40个字节)
return 0;
}
当数组名作为函数参数传递时,就失去了原有特性,退化为一般指针。此时,不能通过sizeof运算符获取数组的长度,不能判断数组的长度时,可能会产生数组越界访问。因此传递数组名时,需要一起传递数组的长度。
// err
void test(int arr[10])
{}
void test(int arr[])
{}
void test(int* arr)
{}
int main()
{
int arr[10] = { 0 };
test(arr);
return 0;
}
// ok
void test(int arr[10], int n)
{}
void test(int arr[], int n)
{}
void test(int* arr, int n)
{}
int main()
{
int arr[10] = { 0 };
int n = sizeof(arr) / sizeof(arr[0]);
test(arr, n);
return 0;
}
二维数组是一维数组的数组,二维数组的数组名也表示数组首元素(第一个一维数组)的地址。
二、指针
2.1 指针和指针变量
指针是内存地址。指针变量是用来存放内存地址的变量,但我们叙述时常把指针变量简称为指针。
#include <stdio.h>
int main()
{
int a = 10; // 在内存中开辟一块空间
int* p = &a; // &操作符取出a的地址
// a占用4个字节的空间,这里是将a的4个字节的第1个字节的地址存放在p中,p就是一个指针变量
return 0;
}
在32位的机器上,地址是由32个0或者1组成的二进制序列,用4个字节的空间来存储,所以一个指针变量的大小是4个字节。
在64位的机器上,地址是由64个0或者1组成的二进制序列,用8个字节的空间来存储,所以一个指针变量的大小是8个字节。
2.2 指针类型
int*类型的指针存放int类型变量的地址,char*类型的指针存放char类型变量的地址……
2.2.1 指针+-整数
指针的类型决定了指针的步长(+-1操作的时候,跳过几个字节)。
int*类型的指针+-1跳过4个字节,char*类型的指针+-1跳过1个字节……
#include <stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("%p\n", &n); // 000000DADACFF4E4
printf("%p\n", pc); // 000000DADACFF4E4
printf("%p\n", pc + 1); // 000000DADACFF4E5
printf("%p\n", pi); // 000000DADACFF4E4
printf("%p\n", pi + 1); // 000000DADACFF4E8
return 0;
}
2.2.2 指针的解引用
指针的类型决定了对指针解引用的时候有多大的权限(能访问几个字节)。
int*类型的指针解引用能访问4个字节,char*类型的指针解引用能访问1个字节……
利用int*类型的指针强制转换成char*类型后只能访问1个字节,来判断当前计算机是大端模式还是小端模式:
#include <stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
1(int型)的补码用十六进制表示为0x00000001。
大端模式:00 00 00 01
低地址<--->高地址
小端模式:01 00 00 00
低地址<--->高地址
*(char*)&a表示取出a的地址,然后强制类型转换为char*,再解引用,此时只能访问一个字节的内容。如果这一个字节的内容为0,为大端模式;如果这一个字节的内容为1,为小端模式。
2.3 野指针
野指针是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
2.3.1 野指针的成因
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
2.3.2 规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放及时置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
#include <stdio.h>
int main()
{
int* p = NULL;
// ...
int a = 10;
p = &a;
if (p != NULL)
{
*p = 20;
}
return 0;
}
2.4 指针运算
2.4.1 指针自增
p++:
- 先使用p
- 再自增p
++p:
- 先自增p
- 再使用p
(*p)++:
- 先使用*p
- 再自增*p
*p++或*(p++):
- 解引用(*)和后置自增(++)优先级相同,结合性都是从右往左,所以*p++等价于*(p++)
- 先使用*p
- 再自增p
++*p或++(*p):
- 先自增(*p)
- 再使用*p
*++p或*(++p):
- 先自增p
- 再使用*p
2.4.2 指针-指针
指针-指针的绝对值是指针之间元素的个数。指向同一块区间的两个指针才能相减。
高地址-低地址=正数,低地址-高地址=负数。
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p1 = arr; // 指向arr[0]
int* p2 = arr + 3; // 指向arr[3]
printf("%d\n", p2 - p1); // 3
printf("%d\n", p1 - p2); // -3
return 0;
}
2.4.3 指针的关系运算
可以用关系运算符进行指针比较。只有在两个指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个元素的相对位置。
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p1 = arr;
int* p2 = &arr[3];
if (p1 < p2)
{
printf("p1 < p2\n");
}
else
{
printf("p1 >= p2\n");
}
// p1 < p2
return 0;
}
2.5 二级指针
int a = 10;
int* pa = &a;
int** ppa = &pa;
a的地址存放在pa中,pa的地址存放在ppa中;pa是一级指针,ppa是二级指针。
32位系统中,定义**a[3][4],则变量占用内存空间为?
a是一个大小为3*4、存放着二级指针的数组。在32位系统中,指针的大小为4Byte。所以该数组占用的内存空间大小为3*4*4=48Byte。
三、指针数组和数组指针
3.1 指针数组
指针数组是存放指针的数组。
int* arr1[10]; // 存放一级整型指针的一维数组
char* arr2[4]; // 存放一级字符指针的一维数组
char** arr3[5]; // 存放二级字符指针的一维数组
int** arr4[3][4]; // 存放二级整型指针的二维数组
3.2 数组指针
数组指针是指向数组的指针。
int* p1[10]; // 指针数组
int(*p2)[10]; // 数组指针
p2先和*结合,说明p2是一个指针变量,然后指针指向的是一个大小为10的整型数组。所以p2是一个指针,指向一个数组,叫数组指针。[]的优先级要高于*的,所以必须加上()来保证p2先和*结合。
3.2.1 数组指针解引用
int arr[5] = { 0 };
int(*p)[5] = &arr;
// p是数组指针,p解引用(*p)表示什么?
*p表示整个数组,拿到数组所有元素,但这样没有任何意义,编译器会把*p转化为数组首元素的地址。但在sizeof(*p)和&(*p)中*p还是整个数组。所以*p相当于数组名。
#include <stdio.h>
int main()
{
int arr[5] = { 0 };
int(*p)[5] = &arr; // p保存的是整个数组的地址
printf("%d\n", sizeof(*p)); // *p是整个数组,大小为5×4=20个字节
printf("%d\n", sizeof(*p + 0)); // *p是数组首元素的地址,大小为4/8个字节(32/64位机器)
printf("%p\n", &(*p)); // 010FFAA8 *p是整个数组,&(*p)是整个数组的地址
printf("%p\n", &(*p) + 1); // 010FFABC &(*p)是整个数组的地址,&(*p)+1跳过整个数组(20个字节)
printf("%p\n", *p + 1); // 010FFAAC *p是数组首元素的地址,*p+1跳过4个字节,是数组第二个元素的地址
return 0;
}
#include <stdio.h>
int main()
{
int arr[3][4] = { 0 };
int(*p)[4] = arr; // p保存的是首行的地址
printf("%d\n", sizeof(*p)); // *p是首行,大小为4×4=16个字节
printf("%d\n", sizeof(*p + 0)); // *p是首行首元素的地址,大小为4/8个字节(32/64位机器)
printf("%p\n", &(*p)); // 009EFB24 *p是首行,&(*p)是首行的地址
printf("%p\n", &(*p) + 1); // 009EFB34 &(*p)是首行的地址,&(*p)+1跳过一行(16个字节)
printf("%p\n", *p + 1); // 009EFB28 *p是首行首元素的地址,*p+1跳过4个字节,是首行第二个元素的地址
return 0;
}
3.2.2 数组指针的使用
3.2.2.1 遍历一维数组
实参为数组名,形参为数组:
#include <stdio.h>
void print(int arr[10], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
实参为数组名,形参为指针:
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
// p=arr=数组首元素的地址
// p+i=数组下标为i的元素的地址
// *(p+i)=数组下标为i的元素的值=p[i]
// printf("%d ", p[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
实参为数组的地址,形参为数组指针:
#include <stdio.h>
void print(int(*p)[10], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(*p + i));
// p=&arr=数组的地址
// *p=数组首元素的地址
// *p+i=数组下标为i的元素的地址
// *(*p+i)=数组下标为i的元素的值=(*p)[i]
// printf("%d ", (*p)[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(&arr, sz);
return 0;
}
3.2.2.2 遍历二维数组
实参为数组名,形参为数组:
#include <stdio.h>
void print(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");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
return 0;
}
实参为数组名,形参为数组指针:
二维数组的数组名表示数组首元素(第一个一维数组)的地址,所以可以用数组指针来接收,指针指向元素个数为5的整型数组。
#include <stdio.h>
void print(int(*p)[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 ", *(*(p + i) + j));
// p=arr=首行的地址
// p+i=i行的地址
// *(p+i)=i行首元素的地址=p[i]
// *(p+i)+j=i行j列元素的地址=p[i]+j
// *(*(p+i)+j)=i行j列元素的值=*(p[i]+j)=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} };
print(arr, 3, 5);
return 0;
}
四、数组参数和指针参数
4.1 当实参是一维数组的数组名时,形参可以是什么?
void test1(int arr[10], int n) // ok 形参是一维数组
{}
void test1(int arr[], int n) // ok 形参是一维数组,数组大小可以省略
{}
void test1(int* arr, int n) // ok 形参是一级指针
{}
void test2(int* arr2[20], int n) // ok 形参是一维指针数组
{}
void test2(int* arr2[], int n) // ok 形参是一维指针数组,数组大小可以省略
{}
void test2(int** arr2, int n) // ok 形参是二级指针
{}
int main()
{
int arr1[10] = { 0 }; // 一维数组
int n1 = sizeof(arr1) / sizeof(arr1[0]);
int* arr2[20] = { 0 }; // 一维指针数组
int n2 = sizeof(arr2) / sizeof(arr2[0]);
test1(arr1, n1);
test2(arr2, n2);
return 0;
}
4.2 当实参是二维数组的数组名时,形参可以是什么?
void test(int arr[3][5], int n) // ok 形参是二维数组
{}
void test(int arr[][5], int n) // ok 形参是二维数组,行数可以省略
{}
void test(int arr[3][], int n) // err 形参是二维数组,列数不可以省略
{}
void test(int arr[][], int n) // err 形参是二维数组,列数不可以省略
{}
void test(int(*arr)[5], int n) // ok 形参是数组指针,指向二维数组的首元素(首行),即一个大小为5的一维数组
{}
void test(int* arr, int n) // err 形参不可以是一级指针
{}
void test(int* arr[5], int n) // err 形参不可以是一级指针数组
{}
void test(int** arr, int n) // err 形参不可以是二级指针
{}
int main()
{
int arr[3][5] = { 0 }; // 二维数组
int n = sizeof(arr) / sizeof(arr[0]);
test(arr, n);
return 0;
}
4.3 当形参是一级指针时,实参可以是什么?
void test(int* p) // 形参是一级整型指针
{}
void test(int* p, int n) // 形参是一级整型指针
{}
int main()
{
int a = 0;
test(&a); // ok 实参是整型变量地址
int* p = &a;
test(p); // ok 实参是一级整型指针
int arr[10] = { 0 };
int n = sizeof(arr) / sizeof(arr[0]);
test(arr, n); // ok 实参是一维整型数组的数组名
return 0;
}
4.4 当形参是二级指针时,实参可以是什么?
void test(int** p) // 形参是二级整型指针
{}
void test(int** p,int n) // 形参是二级整型指针
{}
int main()
{
int a = 0;
int* pa = &a;
test(&pa); // ok 实参是一级整型指针地址
int** ppa = &pa;
test(ppa); // ok 实参是二级整型指针
int* arr[10] = { 0 };
int n = sizeof(arr) / sizeof(arr[0]);
test(arr, n); // ok 实参是一维整型指针数组的数组名
return 0;
}
五、函数、指针和数组
5.1 函数指针
&函数名=函数名=函数的地址。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &Add); // 00B313D4
printf("%p\n", Add); // 00B313D4
// &Add=Add,表示Add函数的地址
return 0;
}
函数指针是指向函数的指针。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 函数指针变量pf保存了Add函数的地址,变量类型为int (*)(int, int)
int (*pf)(int x, int y) = &Add;
/*
int (*pf)(int x, int y) = Add; // Add=&Add
int (*pf)(int, int) = &Add; // 形参可以省略
int (*pf)(int, int) = Add; // Add=&Add,形参可以省略
*/
// 调用Add函数
int sum = (*pf)(3, 5);
/*
int sum = pf(3, 5); // pf(3, 5) = (*pf)(3, 5)
int sum = Add(3, 5);
*/
printf("%d\n", sum);
return 0;
}
《C陷阱与缺陷》中的两段代码:
代码1:
(*(void(*)())0)();
void(*)()是一个函数指针类型,指向的函数没有参数,返回类型为void。
(void(*)())0表示把0强制类型转换为void(*)()类型,把0当做一个函数的地址。
(*(void(*)())0)()表示调用0地址处的函数。
代码2:
void(*signal(int, void(*)(int)))(int);
这是函数声明,声明的函数是signal。
signal(int, void(*)(int))表示signal函数的第一个参数是int类型,第二个参数是void(*)(int)类型,即一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void。
void(*signal(int, void(*)(int)))(int)表示signal函数的返回类型是void(*)(int)类型,即一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void。
简化代码2:
void(*signal(int, void(*)(int)))(int);
typedef void(*pf_t)(int); // 将void(*)(int)类型重命名为pf_t类型
pf_t signal(int, pf_t);
5.2 函数指针数组
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int (*pfArr[2])(int, int) = { Add,Sub }; // 函数指针数组
int ret = pfArr[0](2, 3); // Add(2, 3)
printf("%d\n", ret); // 5
ret = pfArr[1](2, 3); // Sub(2, 3)
printf("%d\n", ret); // -1
return 0;
}
实现两个整数的加减乘除计算器:
使用switch语句:
#include <stdio.h>
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 menu()
{
printf("***************************\n");
printf("***** 1. add 2. sub *****\n");
printf("***** 3. mul 4. div *****\n");
printf("***** 0. exit ****\n");
printf("***************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入2个操作数:>");
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;
}
使用函数指针数组:
#include <stdio.h>
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 menu()
{
printf("***************************\n");
printf("***** 1. add 2. sub *****\n");
printf("***** 3. mul 4. div *****\n");
printf("***** 0. exit ****\n");
printf("***************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int (*pfArr[])(int, int) = { 0,Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
break;
}
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
5.3 指向函数指针数组的指针
int (*pf)(int, int) = &Add; // 函数指针
int (*pfArr[2])(int, int) = { Add,Sub }; // 函数指针数组
int (*(*ppfArr)[2])(int, int) = &pfArr; // 指向函数指针数组的指针
5.4 以下代码分别表示什么
int *p[10]; // 指针数组:数组大小是10,数组元素是int*类型的指针
int (*p)[10]; // 数组指针:指针指向一个数组大小是10,数组元素是int类型的数组
int *p(int); // 函数声明:函数名是p,参数是int类型,返回值是int*类型
int (*p)(int); // 函数指针:指针指向一个参数是int类型,返回值是int类型的函数
int (*p[10])(int); // 函数指针数组:数组大小是10,数组元素是int(*)(int)类型的数组
int (*(*p)[10])(int, int); // 指向函数指针数组的指针:指针指向一个数组大小是10,数组元素是int(*)(int)类型的数组
六、回调函数
回调函数就是一个被作为参数传递的函数。在C语言中,回调函数只能使用函数指针实现。
6.1 qsort函数
#include <stdlib.h>
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
// 执行快速排序
// base 待排数据的起始地址
// num 待排数据的元素个数
// size 待排数据的元素大小(单位:字节)
// compar 函数指针,指向比较两个元素的函数
// 比较函数需要自己编写,规定函数原型为:
// int compar(const void* elem1, const void* elem2)
// 函数返回值的规则如下:
// 当进行升序排序时,
// 如果elem1<elem2,则返回值<0
// 如果elem1=elem2,则返回值=0
// 如果elem1>elem2,则返回值>0
6.1.1 qsort函数排序整型数据
#include <stdio.h>
#include <stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2); // void*类型的变量必须强制类型转换成其他类型才能解引用
}
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(arr[0]), cmp_int);
print(arr, n); // 0 1 2 3 4 5 6 7 8 9
return 0;
}
6.1.2 qsort函数排序结构体类型数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
/*
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
*/
int main()
{
struct Stu s[] = { {"zhangsan",20}, {"lisi",55}, {"wangwu",40} };
int n = sizeof(s) / sizeof(s[0]);
// 按照名字排序
qsort(s, n, sizeof(s[0]), cmp_stu_by_name);
// 按照年龄排序
// qsort(s, n, sizeof(s[0]), cmp_stu_by_age);
printf("%s %d\n", s[0].name, s[0].age);
printf("%s %d\n", s[1].name, s[1].age);
printf("%s %d\n", s[2].name, s[2].age);
return 0;
}
6.2 改写冒泡排序函数
常规冒泡排序函数:
#include <stdio.h>
void bubble_sort(int arr[], int n)
{
// 趟数
for (int i = 0; i < n - 1; i++)
{
// 一趟冒泡排序
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
int n = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, n);
print(arr, n); // 0 1 2 3 4 5 6 7 8 9
return 0;
}
借鉴qsort的设计思想,改写冒泡排序函数,实现对任意类型的数据的排序。
6.2.1 整型数据的冒泡排序函数
#include <stdio.h>
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2); // void*类型的变量必须强制类型转换成其他类型才能解引用
}
void swap(char* buf1, char* buf2, int size)
{
for (int i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort2(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
// 趟数
for (int i = 0; i < num - 1; i++)
{
// 一趟冒泡排序
for (int j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
// 交换
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
void print(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
int n = sizeof(arr) / sizeof(arr[0]);
bubble_sort2(arr, n, sizeof(arr[0]), cmp_int);
print(arr, n); // 0 1 2 3 4 5 6 7 8 9
return 0;
}
6.2.2 结构体类型数据的冒泡排序函数
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
/*
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
*/
void swap(char* buf1, char* buf2, int size)
{
for (int i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort2(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
// 趟数
for (int i = 0; i < num - 1; i++)
{
// 一趟冒泡排序
for (int j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
// 交换
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 55}, {"wangwu", 40} };
int n = sizeof(s) / sizeof(s[0]);
// 按照名字排序
bubble_sort2(s, n, sizeof(s[0]), cmp_stu_by_name);
// 按照年龄排序
// bubble_sort2(s, n, sizeof(s[0]), cmp_stu_by_age);
printf("%s %d\n", s[0].name, s[0].age);
printf("%s %d\n", s[1].name, s[1].age);
printf("%s %d\n", s[2].name, s[2].age);
return 0;
}
七、数组练习题
7.1 一维数组
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4 }; // a是数组名
printf("%d\n", sizeof(a)); // 16 a是整个数组
printf("%d\n", sizeof(a + 0)); // 4/8 a是数组首元素的地址,a+0也是数组首元素的地址
printf("%d\n", sizeof(*a)); // 4 a是数组首元素的地址,*a是数组首元素
printf("%d\n", sizeof(a + 1)); // 4/8 a是数组首元素的地址,a+1跳过4个字节,是数组第二个元素的地址
printf("%d\n", sizeof(a[1])); // 4 a[1]是数组第二个元素
printf("%d\n", sizeof(&a)); // 4/8 &a是整个数组的地址
printf("%d\n", sizeof(*&a)); // 16 *&a是整个数组
printf("%d\n", sizeof(&a + 1)); // 4/8 &a是数组的地址,&a+1跳过整个数组,也是地址
printf("%d\n", sizeof(&a[0])); // 4/8 &a[0]是数组首元素的地址
printf("%d\n", sizeof(&a[0] + 1)); // 4/8 &a[0]是数组首元素的地址,&a[0]+1跳过4个字节,是数组第二个元素的地址
return 0;
}
7.2 字符数组
sizeof和strlen的区别:
- sizeof运算符计算数据类型或变量长度(单位:字节)
- strlen函数计算字符串长度(从字符串开始到'\0'之间的字符数,不包括'\0'本身)
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr)); // 6 arr是整个数组
printf("%d\n", sizeof(arr + 0)); // 4/8 arr是数组首元素的地址,arr+0还是数组首元素的地址
printf("%d\n", sizeof(*arr)); // 1 arr是数组首元素的地址,*arr是数组首元素
printf("%d\n", sizeof(arr[1])); // 1 arr[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]是数组首元素的地址,&arr[0]+1跳过1个字节,是数组第二个元素的地址
printf("%d\n", strlen(arr)); // 随机值 arr是数组首元素的地址,数组中没有\0,后面是否有\0、在什么位置是不确定的
printf("%d\n", strlen(arr + 0)); // 随机值 arr是数组首元素的地址,arr+0还是数组首元素的地址,同上
printf("%d\n", strlen(*arr)); // err *arr是数组首元素'a',ASCII码值是97,strlen把97当成地址,会非法访问内存
printf("%d\n", strlen(arr[1])); // err arr[1]是数组第二个元素'b',ASCII码值是98,同上
printf("%d\n", strlen(&arr)); // 随机值 &arr是整个数组的地址,数组的地址也是指向数组起始位置,同strlen(arr)
printf("%d\n", strlen(&arr + 1)); // 随机值 &arr+1跳过整个数组,后面是否有\0、在什么位置是不确定的
printf("%d\n", strlen(&arr[0] + 1)); /* 随机值 &arr[0]是数组首元素的地址,&arr[0]+1跳过1个字节,是数组第二个元素的地址,
数组中没有\0,后面是否有\0、在什么位置是不确定的*/
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef"; // 等价于char arr[] = { 'a','b','c','d','e','f','\0' };
printf("%d\n", sizeof(arr)); //7 arr是整个数组
printf("%d\n", sizeof(arr + 0)); //4/8 arr是数组首元素的地址,arr+0还是数组首元素的地址
printf("%d\n", sizeof(*arr)); //1 arr是数组首元素的地址,*arr是数组首元素
printf("%d\n", sizeof(arr[1])); //1 arr[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]是数组首元素的地址,&arr[0]+1跳过1个字节,是数组第二个元素的地址
printf("%d\n", strlen(arr)); //6 arr是数组首元素的地址,计算从数组首元素到第一个\0的字符数
printf("%d\n", strlen(arr + 0)); //6 arr是数组首元素的地址,arr+0还是数组首元素的地址,同上
printf("%d\n", strlen(*arr)); //err *arr是数组首元素'a',ASCII码值是97,strlen把97当成地址,会非法访问内存
printf("%d\n", strlen(arr[1])); //err arr[1]是数组第二个元素'b',ASCII码值是98,同上
printf("%d\n", strlen(&arr)); //6 &arr是整个数组的地址,数组的地址也是指向数组起始位置,同strlen(arr)
printf("%d\n", strlen(&arr + 1)); //随机值 &arr+1跳过整个数组,后面是否有\0、在什么位置是不确定的
printf("%d\n", strlen(&arr[0] + 1));/*5 &arr[0]是数组首元素的地址,&arr[0]+1跳过1个字节,是数组第二个元素的地址,
数组中没有\0,后面是否有\0、在什么位置是不确定的*/
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
const char* p = "abcdef"; // 把字符串常量首字符a的地址放到指针变量p中
printf("%d\n", sizeof(p)); //4/8 p是首字符的地址
printf("%d\n", sizeof(p + 1)); //4/8 p+1跳过1个字节,是第二个字符的地址
printf("%d\n", sizeof(*p)); //1 *p是首字符
printf("%d\n", sizeof(p[0])); //1 p[0]=*(p+0)=*p,是首字符
printf("%d\n", sizeof(&p)); //4/8 &p是指针变量p的地址
printf("%d\n", sizeof(&p + 1)); //4/8 &p+1跳过p,也是地址
printf("%d\n", sizeof(&p[0] + 1)); //4/8 &p[0]是首字符的地址,&p[0]+1跳过1个字节,&p[0]+1是第二个字符的地址
printf("%d\n", strlen(p)); //6 p是首字符的地址,计算从首字符到第一个\0的字符数
printf("%d\n", strlen(p + 1)); //5 p+1跳过1个字节,是第二个字符的地址,计算从第二个字符到第一个\0的字符数
printf("%d\n", strlen(*p)); //err p是首字符'a',ASCII码值是97,strlen把97当成地址,会非法访问内存
printf("%d\n", strlen(p[0])); //err p[0]是首字符'a',同上
printf("%d\n", strlen(&p)); //随机值 &p是指针变量p的地址,后面是否有\0、在什么位置是不确定的
printf("%d\n", strlen(&p + 1)); //随机值 &p+1跳过p,后面是否有\0、在什么位置是不确定的
printf("%d\n", strlen(&p[0] + 1)); //5 &p[0]是首字符的地址,&p[0]+1跳过1个字节,是第二个字符的地址,同strlen(p+1)
return 0;
}
7.3 二维数组
#include <stdio.h>
int main()
{
int a[3][4] = { 0 }; // a是二维数组的数组名,a[i]是下标为i的一维数组的数组名
printf("%d\n", sizeof(a)); //48 a是整个数组
printf("%d\n", sizeof(a[0][0])); //4 a[0][0]是首行首列元素
printf("%d\n", sizeof(a[0])); //16 a[0]是整个首行
printf("%d\n", sizeof(a[0] + 1)); //4/8 a[0]是首行首元素的地址,a[0]+1跳过4个字节,是首行第二个元素的地址
printf("%d\n", sizeof(*(a[0] + 1))); //4 a[0]+1是首行第二个元素的地址,*(a[0]+1)是首行第二个元素
printf("%d\n", sizeof(a + 1)); //4/8 a是首行的地址,a+1跳过一行,是第二行的地址
printf("%d\n", sizeof(*(a + 1))); //16 a+1是第二行的地址,*(a+1)是整个第二行
printf("%d\n", sizeof(&a[0] + 1)); //4/8 &a[0]是整个首行的地址,&a[0]+1跳过一行,是第二行的地址
printf("%d\n", sizeof(*(&a[0] + 1))); //16 &a[0]+1是第二行的地址,*(&a[0]+1)是整个第二行
printf("%d\n", sizeof(*a)); //16 a是首行的地址,*a是整个首行
printf("%d\n", sizeof(a[3])); /*16 a[3]理论上是第4行,虽然没有第4行,但类型能够确定,大小就是确定的,
sizeof只是计算a[3]的大小,并不会访问对应内存,所以不会报错*/
return 0;
}
八、指针练习题
- 例题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)); // 2,5
return 0;
}
- 例2
#include <stdio.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}* p; // p是一个结构体指针变量
// X86环境下演示:
// 假设p的值为0x100000。 如下表表达式的值分别为多少?
// 已知,结构体Test类型的变量大小是20个字节
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1); // 0x100014 struct Test*类型+1跳过20个字节
printf("%p\n", (unsigned long)p + 0x1); // 0x100001 整型+1直接计算
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004 unsigned int*类型+1跳过4个字节
return 0;
}
- 例3
- 1(int型)的补码用十六进制表示为0x00000001,小端模式:01 00 00 00(低地址<--->高地址)。
#include <stdio.h>
// 假设机器为小端存储模式
int main()
{
int a[4] = { 1,2,3,4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1); // 把数组首元素的地址的数值+1,再转换为地址
printf("%x,%x", ptr1[-1], *ptr2); // ptr1[-1]=*(ptr1-1)
// 4,2000000
return 0;
}
- 例4
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1),(2, 3),(4, 5) };
// exp1,exp2,exp3,...,expN:逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果
// 不等于int a[3][2] = {{0,1}, {2,3}, {4,5}};
// 等价于int a[3][2] = { 1, 3, 5 };
// 1 3
// 5 0
// 0 0
int* p;
p = a[0];
printf("%d", p[0]); // 1
return 0;
}
- 例5
#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]); // &p[4][2]=&*(*(p+4)+2)=*(p+4)+2
// FFFFFFFC,-4
return 0;
}
指针-指针的绝对值是指针之间元素的个数。指向同一块区间的两个指针才能相减。
高地址-低地址=正数,低地址-高地址=负数。
*(p+4)+2-&a[4][2]=-4
//-4
//原码:10000000000000000000000000000100
//反码:11111111111111111111111111111011
//补码:11111111111111111111111111111100--十六进制-->FFFFFFFC
- 例6
#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)); // 10,5
return 0;
}
- 例7
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa); // at
return 0;
}
- 例8
#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); // POINT
printf("%s\n", *-- * ++cpp + 3); // ER
printf("%s\n", *cpp[-2] + 3); // *cpp[-2]+3=**(cpp-2)+3 ST
printf("%s\n", cpp[-1][-1] + 1); // cpp[-1][-1]+1=*(*(cpp-1)-1)+1 EW
return 0;
}
**++cpp:
*--*++cpp+3:
**(cpp-2)+3:
*(*(cpp-1)-1)+1: