c的指针整理
基础知识
c的指针指向的是某个内存地址。
指针变量的定义
int *p;
char *p;
说明:
1、指针变量存放的是地址类型。
2、定义指针变量的时候设定的变量类型,确定的是从指针存的首地址开始后面读取多少个字节。(int 为4个,char为1个),而指针变量的大小是固定的,大小看设备,一般是4个字节(32位)。
int a = 51;
int* p = &a;
printf("%d", sizeof(p));
结果:
4
和指针有关的运算符
运算符 | 作用 |
---|---|
* | 定义指针时使用,和取值时一个符号 |
& | 取址运算符 |
* | 取值运算符 |
* //定义指针时使用,和取值时一个符号
& //取址运算符
* //取值运算符
/*举例*/
int *p=&a; //定义指针和取址
printf("%d",*p); //取值
说明:
1、当需要读取某个变量的地址时,使用取址运算符
2、当对指针变量操作,需要读取指针所指向的变量的值时,使用取值运算符
简单的用法
#include <stdio.h>
int main (void)
{
int a=0;
char b ='A';
int * ap=&a;
char * bp=&b;
printf("%d\n",a);
printf("%d\n",*ap);
printf("%c\n",b);
printf("%c\n",*ab);
}
结果:
0
0
A
A
注意:指针变量一定要初始化,不能对未初始化的指针进行操作
void指针和NULL指针
void指针
void指针代表的是无类型,一般使用时不能用来定义变量,但是可以定义指针变量。
int a;
void b;//报错
void *c;//可以正常定义
结果:
严重性 代码 说明 项目 文件 行 禁止显示状态 错误 C2182 “b”: 非法使用“void”类型
严重性 代码 说明 项目 文件 行 禁止显示状态 错误(活动) E0070 不允许使用不完整的类型
○那么void指针是怎么使用的?
- void类型的指针变量可以指向任意类型的指针,但是不能对其直接解引用,因为程序不知道具体的数据大小。
- 如需对其解引用,可以只用强制转换(*)将void指针变量转换为正确的指针变量。
- 比较特殊的是char类型的字符串,因为char[]打印原理是从其实地址打印一直打印到\0结束,所以看上去void类型指针指向字符串时,似乎可以打印成果。但是这是极不规范的写法,并且也是错误的。所以一般情况也需要对其进行强制转换。
int main(void)
{
int a=10;
void *c;//可以正常定义
c = &a;
printf("a的地址:%p\n", &a);
printf("*c的内容:%p\n", c);
printf("对void *c解引用:%d\n", *((int *)c));
return 0;
}
结果:
a的地址:0028FD38
*c的内容:0028FD38
对void *c解引用:10
NULL指针
NULL在c语言中是一个宏定义:#define NULL ((void *)0)
表示指向一个不被使用的地址。
NULL指针的作用:当编写程序的时候,定义的指针没有确定具体指向的地址的时候,可以使其等于NULL,这是一个良好的使用习惯。
int main(void)
{
int* P1;
int* P2=NULL;
printf("P1的内容:%p\n", P1);
printf("P2的内容:%p \n", P2);
return 0;
}
结果:
严重性 代码 说明 项目 文件 行 禁止显示状态 错误 C4700 使用了未初始化的局部变量“P1”
为什么要使用NULL指针?
- 当指针没有初始化时,他会随机指向一个地址,这是没有意义的。如果你在程序中不小心使用了该指针,会发生意想不到的问题。当然这个在编译的时候就会报错。
- 当不知道指针变量该指向谁的时候,可以先指向NULL。这样来避免该指针变成野指针(迷途指针)
- 这是一个很重要的变成习惯。
指向指针的指针
当使用非指针变量(如int)定义时,储存的数据是一个数(如int存储的是整型数据)。
而使用指针变量定义时,存储数据认定为是一个地址。
那么对比下面两种定义方式。
int a = 10;
int *p1 = &a;
int *p2= &p1;
和
int a = 10;
int *p1 = &a;
int **p2= &p1;
考虑输出: printf(“a的内容:%p\n”, **p2);
第一种:因为p2是指针变量,第一次解引用,我们得到的是p1的地址。注意,此时程序认为p1地址下存储的数据是一个数,而不是一个地址,所以当我们需要第二次解引用读取p1存储的a地址下的数据时,程序时会报错。因为不能对一个数进行解引用。
第二种:因为p2是指向指针的指针变量,第一次解引用,我们得到的是p1的地址。此时程序认为p1的数据类型是一个指针,所以p1地址下存储的是一个地址。当我们第二次解引用读取p1存储的a地址下的数据时,程序会读取a所在内存地址中存储的值。
总的来说,当需要指针指向一个指针的时候,要声明指向的数据类型是一个指针类型。否则,地址会被认定为仅仅是一个数。
指针和数组的关系
**明确:**一个指针变量,在存放在某个内存下,其存放内容是一个变量的地址。
不管这个变量是整形、数组还是字符串,他存放的永远存放的首地址。
例如:
int a=1 为4个字节其存放在地址
0xXXXXXX100000000
0xXXXXXX200000000
0xXXXXXX300000000
0xXXXXXX400000001
那么对于指向a的指针p,它在其他内存上开辟4个字节,并只存放0xXXXXXX1这个地址。因为我们定义指针p是int类型的,所以它会在起始位置自动后面数4个字节。
知道了这个关系,我们也就很好理解指针和数组的关系
指针和数组的例子
int main(void)
{
char* p;
char str[128];
/*之前的操作*/
printf("输入数组");
scanf("%s", str);
printf("数组是:%s\n", str);
/*验证第一个 数组名 就是 数组第一个元素 的地址*/
printf("str: %p\n", str);
printf("&str[0]: %p\n", &str[0]);
/*如何用一个指针指向数组*/
p = str;//方法一
//p = &str[0];//方法二
printf("%s\n", p);
printf("%c\n", *p);
printf("%c\n", *(p+1));
/*可以用数组下标的方法调用指针,也可以用指针的方法对数组进行调用*/
printf("%c\n", p[0]);
printf("%c\n", p[1]);
printf("%c\n", *str);
printf("%c\n", *(str + 1));
return 0
结果:
输入数组ab
数组是:ab
str: 00F5F76C
&str[0]: 00F5F76C
ab
a
b
a
b
a
b
指针和数组的区别
需要注意:数组和指针虽然很相似,但是是不一样的
我们在上面进行了说明,数组名就是数组第一个元素的地址。但是,这种说法也不是完全正确的
首先我们先简单解释:
左值和右值:左值一般是指赋值符号左边的值,一般情况下是不能被修改的。不是左值的值叫做右值
此时,我们就能发现指针和数组的不同了,举个例子:
int main(void)
{
char* p;
char str[128] = {"ABCDEFGHIGK"};
int total = 0;
while(*(str)++ != '\0')
{
total++;
}
return 0;
}
结果:
严重性 代码 说明 项目 文件 行 禁止显示状态
错误(活动) E0137 表达式必须是可修改的左值 Project2
可以看到,作为数组名称的str为左值,不能进行修改。那么我们应该怎么做呢?
修改如下:
int main(void)
{
char* p;
char str[128] = {"ABCDEFGHIGK"};
p = str;
int total = 0;
while(*p++ != '\0')
{
total++;
}
printf("%d", total);
return 0;
}
结果:
11
你会发现数组的函数名作为一个左值,不能在某些情况下使用。
指针数组和数组指针
说明:判断一个表达到底是什么,只要看最后是什么。如 指针数组,它结尾是数组,所以这个表达的本质就是一个数组。
指针数组
int *p[5];
理解为:有一个数组,数组里的每一个元素都是指向int类型的指针。
int main(void)
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int *p[5] = { &a,&b,&c,&d,&e };
for (size_t i = 0; i < 5; i++)
{
printf("%p\n", &p[i]);
}
printf("%p\n", &a);
}
数组指针
int (*p)[5];
理解为:首先(*p)确定它为一个指针,指向的是有5个元素的数组
int main(void)
{
int str[5] = { 1,2,3,4,5 };
int(*a)[5] = &str;
for (size_t i = 0; i < 5; i++)
{
printf("%d\n", *(*a+i));
}
}
这里比较难理解。
首先当定义数组指针的时候,int(*a)[5] = &str;
这个指针相当于把数组看作了一个整体,所以在赋值的时候,要对该数组取地址。
其次,在打印数组中的元素时,*(*a+i)
首先需要先解引用一次,找到数组所在的地址,然后再解引用,调出数组用对应元素的地址。
指针和二维数组
首先我们先看一个二维数组array[4][5]
array[0][0]——>datadata data data data
array[1][0]——>datadata data data data
array[2][0]——>datadata data data data
array[3][0]——>datadata data data data
需要理解二位数组存是怎么存放的。
定义:char array[4][5] = { "ABCD","EFGH","IJKL","MNOP" };
array[0] | 0x00000000 | A | array[1] | 0x00000005 | E | array[2] | 0x0000000A | I | array[3] | 0x000000F | M |
---|---|---|---|---|---|---|---|---|---|---|---|
0x00000001 | B | 0x00000006 | F | 0x0000000B | J | 0x00000010 | N | ||||
0x00000002 | C | 0x00000007 | G | 0x0000000C | K | 0x00000011 | O | ||||
0x00000003 | D | 0x00000008 | H | 0x0000000D | L | 0x00000012 | P | ||||
0x00000004 | \n | 0x00000009 | \n | 0x0000000E | \n | 0x00000013 | \n |
下面对几种表达进行说明:
- array[0]代表了什么:代表了二维数组第一行的的首地址。同理array[1]代表了第二行的首地址。
- *(array+1)代表了什么:array+1表示指向array的下一行(里面存放的是array[1]的地址),进行解引用得到array[1][0]的地址。
- 具体内容看另一篇文章:指针和二维数组
指向指针的指针和二维数组
指向指针的指针是不能去表示二维数组的。
int main(void)
{
int str[3][4] = { {123},{456},{789} };
int** p = str;
printf("str+1的地址:%p\n",str+1);
printf("p+1的地址:%p\n", p + 1);
return 0;
}
结果:
str+1的地址:0048FDC0
p+1的地址:0048FDB4
你会发现地址是不一样的,这是由于对于指向指针的指针p来说,他的长度只是一个int的长度,而对于二维数组,str+1的长度,是整一列。所以当你希望用指向指针的指针操作二维数组时,就无法正确操作。
那么我们需要怎么去处理呢?
使用数组指针
int main(void)
{
int str[3][4] = { {123},{456},{789} };
int (*p)[4] = str;
printf("str+1的地址:%p\n",str+1);
printf("p+1的地址:%p\n", p + 1);
return 0;
}
结果:
str+1的地址:001BFA2C
p+1的地址:001BFA2C
常量与指针
在c语言中,我们怎么去定义常量?
- 使用
#define a 10
宏定义去实现 - 使用
const int a = 10;
来初始化变量
当然const是能修饰指针的。那么在定义变量的时候会有一下几种格式。
- 对于part1,不使用const进行修饰,我们可以在程序中随意的改变变量的值。
- 对于part2, 用const修饰过之后,不管是普通的变量还是指针,都不能进行更改。
- 对于part3,当使用
const int *p1
对指针修饰时,其代表该指针指向的数据是const类型,所以不能对指针指向的地址中的内容进行更改。但是指针本身不是const的,所以指针可以更换指向的目标。 - 对于part4,此时使用
const int * const p1
,其表示指针本身不能被修改,指针指向的地址中的数据也不能被更改。 - 当然还有
int *const p3 =&a
这种表达,其代表指针指代的地址不能更改,但是指向的地址中的内容可以进行更改。
他们的称呼:
表达 | 称呼 |
---|---|
const int a | 常量 |
const int *p1 | 指向常量的指针 |
int *const p1 | 常量指针 |
const int *const p1 | 指向常量的常量指针 |
更为复杂的:指向常量的常量指针的指针 const int *const *p
其表达的意思是指针变量p指向的是一个常量指针,这个常量指针指向的地址下存放的是一个常量。
总的来说,确定好const修饰的地方,就可以很好的进行判断程序的结构。
指针和函数
函数
函数的基本结构是:
类型名 函数名(参数列表)
{
函数体;
}
对于函数,在程序开头,还要进行声明。
声明格式:
类型名 函数名(参数列表)
此时除了函数名,其他的定义名称都可以省略,只需要保留数据的类型即可。
传值和传址
在调用函数时,输入的参数叫做实参,而在函数体参数列表中的数据叫做形参。
当形参定义的是普通的变量的时候,实参也应该传递普通的变量,这个过程叫做传值。
当形参定义的是指针变量的时候,实参应该传递的是一个地址,此时的过程叫做传址。
指针函数和函数指针
- 指针函数:
int *p(){}
:这是一个函数,返回值是一个指针。 - 函数指针:
int (*p)( ){}
:这是一个指针,其指向的地址是某个函数的返回值。
在实际使用时,可以进行各种嵌套,比如函数指针作为函数的参数等等。