【C/C++】系列文章目录
第三节【C/C++】初识C语言语句、函数、数组、运算符、关键字
文章目录
前言
此章节为前述章节指针部分的进阶补充,包含指针类型的含义,野指针,二级指针,指针数组,数组指针,数组传参等。
一、指针是什么?
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
指针变量我们可以通过 & (取地址操作符)取出变量的内存真实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
p就是一个指针变量。
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
二、指针类型
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
那么指针类型有什么作用呢?
可以看到char*类型的指针+1,相差1个字节;int*类型的指针+1,相差4个字节。
所以:指针的类型决定了指针向前或者向后走一步有多大(距离)。
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
n在内存中的存储:
可以看到char*类型的指针解引用赋值,只改变了1个字节的内容。
int*类型的指针解引用赋值,改变了4个字节的内容。
所以:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
总结:
指针的类型决定了指针向前或者向后走一步有多大(距离)。
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
void*类型的指针可以存放任意类型指针,但无法解引用和运算。
三、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的) 。
1.造成野指针的原因
1.未初始化
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
直接报错。
2.越界访问
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3.使用已经释放的空间
动态分配的空间被释放后继续使用。
int main()
{
int* p=(int*)malloc(10 * sizeof(int));
free(p);//分配的空间已经被释放
*p = 20;//错误,p是野指针
return 0;
}
2.如何规避野指针
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
int main()
{
int *p = NULL;
int a = 10;
p = &a;
if(p != NULL)
{
*p = 20;
}
return 0;
}
四、指针运算
1.指针+- 整数
指针加减整数,就是走过多少地址距离,通过解引用可以找到对应地址的值。
2.指针-指针
可以看到指针-指针得出的是两个指针之间元素的个数。
int main()
{
int arr[5] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int* p2 = &arr[4];
printf("%d\n", p2 - p);//4
return 0;
}
3.指针的关系运算
#define N_VALUES 5
int main()
{
int values[N_VALUES];
int* vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
return 0;
}
常理来讲,以上代码更习惯以下写法:
int main()
{
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
return 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针进阶
1.指针和数组
可见数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址。
然而:
p可以p++,而arr不可以arr++。arr是数组首元素地址时是常量,常量无法++。
以上代码可修改为:
int main()
{
int arr[5] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;//数组名即数组首元素地址
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d %d\n", *(p+i),*(arr+i));
}
return 0;
}
*(arr+i) ===> arr[i]。
2.二级指针
指针变量也是变量,是变量就有地址,存放指针变量的地址的变量就是二级指针 。
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
printf("%p\n", &a);
printf("%p\n", pa);
printf("%p\n", &pa);
printf("%p\n", ppa);
return 0;
}
可以看到pa中存放a的地址,解引用可以找到a的值。
ppa中存放的是pa的地址,解引用可以找到pa的值。
pa是一级指针,ppa是二级指针。
*pa = 20;//*pa找到a并赋值20
**ppa=30;//*ppa找到pa在*解引用找到a并赋值30
3.字符指针
写法1:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'x';
return 0;
}
写法2:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'x';
const char* pstr = "hello bit.";
printf("%s\n", pstr);
printf("%c\n", *pstr);
return 0;
}
pstr中放了一个地址,通过解引用可以看到值是h,所以是h的地址。写法2就是将字符串的首地址放入pstr中,“hello"是字符串常量,字符串常量只开辟一个空间,仅一个地址。
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
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;
}
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。
4.指针数组
指针数组是一个数组,每个元素是指针。
int* arr1[10]; //整型指针的数组
char *arr2[3]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
char* arr2[3] = { "hello", "haha", "www" };
说明:arr2先跟[3]结合所以是数组且有3个元素,每个元素类型是char *类型。
5.数组指针
数组指针是一种指针,指向数组的指针。
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;
return 0;
}
说明:parr先跟*结合说明parr是一个指针变量,指向的变量是int [5]类型的数组(有5个整型元素的数组)。
&arr 表示的是整个数组的地址,而不是数组首元素的地址。
本例中 &arr 的类型是: int(*)[5] ,是一种数组指针类型,我们就知道parr的类型是int(*)[5]。
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是20。
//应用
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
puts("-------------------------------------------");
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
示例:
int arr[5];//arr是一个数组,含有5个元素,每个元素都是整型
int *parr1[10];//parr1是一个数组,含有10个元素,每个元素都是一个整型指针
int (*parr2)[10];//parr2是一个指针变量,指向一个数组,这个数组含有10个元素,每个元素都是整型
int (*parr3[10])[5];//parr3是一个数组,含有10个元素,每个元素都是一个数组(此数组含有5个元素,每个元素都是一个整型指针)。
6.数组参数、指针参数
1.一维数组传参
void test(int arr[])//ok
{}//数组形式传参
void test(int arr[10])//ok
{}//数组形式传参
void test(int* arr)//ok
{}//arr是数组首元素地址,指针接收,数组首元素是int类型,所以指针指向int
void test2(int* arr[20])//ok
{}//数组形式传参
void test2(int** arr)//ok
{}//数组首元素是一个int*指针,*arr结合代表是指针变量,指向int*类型的数据
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
2.二维数组传参
void test(int arr[3][5])//ok
{}//数组形式传参
void test(int arr[][])// no
{}//数组形式传参,但是二维数组只能省略行,不能省略列
void test(int arr[][5])// ok
{}//数组形式传参
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)// no
{}//指针形式,二维数组首元素是一个一位数组,
//应该接收一个数组指针,指向的数组含有5个整型元素,而此参数是接受的一个整型指针
void test(int* arr[5])// no
{}// arr跟[]结合是一个数组,含有5个元素,每个元素是个整型指针,不符合数组指针
void test(int(*arr)[5])//ok?
{}//指针形式,arr和*结合,arr是一个指针变量指向一个数组(含有5个整型元素的数组)
//符合含有5个整型的数组
void test(int** arr)//no
{}//*arr代表arr是一个指针变量,指向的是整型指针int*,而应该接收一维数组
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;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
void test1(int* p)
{}
//test1函数能接收什么参数?
//1.可以接收int*的整型指针,int x = 0;int*px = &x; test1(px);
//2.可以接收一维数组int arr[10] = { 0 }; test1(arr);
//3.可以接收对应的变量地址,如上例:test1(&x);
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;
}
void test(char** p)
{}
//test函数能接收什么参数?
//1.可以接收字符指针的地址
//2.可以接收二级字符指针
//3.可以接收一个含有字符指针的数组的数组名
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];//数组元素都是字符指针
test(&pc);
test(ppc);
test(arr);
return 0;
}
7.函数指针
函数指针-存放函数的指针。
void test()
{
printf("hehe\n");
}
void (*pfun1)();
函数名==&函数名,函数名就是地址。
解释以下两段代码:
代码1
(*(void (*)())0)();
调用0地址处的函数
该函数无参,返回的是void类型
1. void (*)() - 函数指针类型
2. (void (*)())0 - 对0进行强制类型转换,被解释为一个函数地址
3. *(void (*)())0 - 对函数地址进行解引用,找到0地址处的函数
3. (*(void (*)())0)() - 调用0地址处的函数
代码2
void (*signal(int, void(*)(int)))(int);
1. signal先跟()结合,说明signal是一个函数
2. signal函数的第一个参数是一个整型int,第二个参数是一个函数指针
该函数指针,指向一个参数是int,返回类型是是void的函数
3. signal函数的返回类型也是一个函数指针
该函数指针,指向一个参数是int,返回类型是是void的函数
8.函数指针数组
函数指针数组—存放函数指针的数组。
int Add(int x,int y)
{
}
int Sub(int x, int y)
{
}
int main()
{
int(*arr[2])(int x, int y) = { Add, Sub };//转移表
return 0;
}
解释:1.arr先跟[]结合,说明arr是一个数组,
2.arr数组含有两个元素,每个元素都是一个函数指针。
//该函数指针参数是两个int,返回类型也是int。
9.指向函数指针数组的指针
指向函数指针数组的指针是一个 指针。
指针指向一个 数组 ,数组的元素都是 函数指针。
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
10.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数
的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进
行响应。
//qosrt函数的使用者得实现一个比较函数
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;
}
如上:int_cmp函数就是回调函数,qsort函数通过函数指针实现调用。
总结
本章节包含指针类型的含义,野指针,二级指针,指针数组,数组指针,数组传参,函数指针,函数指针数组,指向函数指针数组的指针等。