目录
一、const关键字和指针
关于指针变量的理解,主要涉及两个指针变量 (p本身,和 *p这个变量)
而我们的 const 只能修饰一种变量,所以我们要搞清楚 const 到底在修饰谁。
1、const修饰指针的4种形式
(1)const关键字,在C语言中用来修饰变量,表示这个变量是常量。
(2)const修饰指针有4种形式,区分清楚这4种即可全部理解const和指针。
第一种:const int *p; p本身不是cosnt的,而p指向的变量是const的
第二种:int const *p; p本身不是cosnt的,而p指向的变量是const的
第三种:int * const p; p本身是cosnt的,p指向的变量不是const的
第四种:const int * const p; p本身是cosnt的,p指向的变量也是const的
规则:const离谁近,谁就不能被修改;
理解什么是谁不能修改(举例 *p不能修改)
int a = 0;
const int *p;
p = &a;
*p = 111;
编译的时候就发生了错误
error: assignment of read-only location ‘*p’ ----- 只读位置' *p '的赋值
assignment : 赋值,
int a = 5;
// 第一种
const int *p1; // p本身不是cosnt的,而p指向的变量是const的
// 第二种
int const *p2; // p本身不是cosnt的,而p指向的变量是const的
// 第三种
int * const p3; // p本身是cosnt的,p指向的变量不是const的
// 第四种
const int * const p4;// p本身是cosnt的,p指向的变量也是const的
*p1 = 3; // error: assignment of read-only location ‘*p1’
p1 = &a; // 编译无错误无警告
*p2 = 5; // error: assignment of read-only location ‘*p2’
p2 = &a; // 编译无错误无警告
*p3 = 5; // 编译无错误无警告
p3 = &a; // error: assignment of read-only variable ‘p3’
p4 = &a; // error: assignment of read-only variable ‘p4’
*p4 = 5; // error: assignment of read-only location ‘*p4’
2、const修饰的变量真的不能改吗?
(1)课堂练习说明:const修饰的变量其实是可以改的(前提是gcc环境下)。
const int a = 5;
//a = 6; // error: assignment of read-only variable ‘a’
int *p;
p = (int *)&a; // 这里报警高可以通过强制类型转换来消除
*p = 6;
printf("a = %d.\n", a); // a = 6,结果证明const类型的变量被改了
(2)在某些单片机环境下,const修饰的变量是不可以改的。const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。
(3)在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
(4)更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。
3、const究竟应该怎么用
const是在编译器中实现的,编译时检查,并非不能骗过。所以在C语言中使用const,就好象是 一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的。
二、深入学习数组
变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度。
1、数组中几个关键符号(a ,a[0] ,&a ,&a[0])的理解
结合左值和右值来进行理解
放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;
当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;
当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。
总结:
做左值:我们希望使用的是那个空间。
做右值:我们希望使用的是空间里面的数据。
int a[10];
a 做左值 :表示整个数组的所有空间(10*4=40)
因为规定我们数组是进行单个空间进行操作的, 所以我们的 a 不可以做左值。
a做右值 :表示数组首元素的地址 , 等同于 &a[0]
a[0]做左值:做左值时表示数组第0个元素对应的内存空间(连续4字节)
a[0]做右值:表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)
&a就是数组名a取地址,字面意思来看就应该是整个数组的地址。(10*4=40个字节)
&a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。);
&a做右值时表示整个数组的首地址。
&a[0]字面意思就是数组第0个元素的首地址
搞清楚[]和&的优先级,[]的优先级要高于&,所以a先和[]结合再取地址
&a[0]做左值时:本质也是一个常量
&a[0]做右值时:表示数组首元素的值(也就是数组首元素对应的内存空间中存储的那个数值)。做右值时&a[0]等同于a。
解释:为什么数组的地址是常量?
因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过&a来获取这个分配的地址,却不能去用赋值运算符修改它。
补充:
1:&a和a做右值时的区别:
&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。
2:&a是常量,不能做左值。
3:a做左值代表整个数组所有空间,所以a不能做左值。
三、数字和指针
1、以指针方式来访问数组元素
前情:
1、我们的数组只能进行单个访问,不能整体访问。 我们有两种访问方式,指针访问和数组访问。
2、指针的加减法运算:(对应于指针加减 相应类型的字节数)
在32位操作系统当中:(long 和 int其实一样)
char :1个字节
short : 2个字节
int: 4个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节
注:指针的大小永远都是 4 字节(在32位地址总线当中),因为它必须表示一个地址。
int *p = (int *)1000;
printf("%d\n",p+1);//1004
printf("%d\n",p+4);//1016
printf("%d\n",(char *)p+4);//1004
printf("%d\n",(short *)p+4);//1008
printf("%d\n",(double *)p+4);//1032
printf("%d\n",(unsigned long long )p+1);//1001
printf("%d\n",(int ***)p+1); //1004记住是去掉一个*号后求sizeof ,去掉一个*号,它本质还是一个指针
printf("%d\n",(double **)p+1);//1004
printf("%d\n",(float **)p+10);//1040
(2)数组格式访问数组元素是:数组名[下标]; (注意下标从0开始)
(3)指针格式访问数组元素是:*(指针+偏移量); 如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标;指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要考虑叠加了。
(4)数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。
在编译器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已。所以用指针方式来访问数组才是本质的做法。
四、指针与强制类型转换
1、变量数据类型的含义
一个变量的数据类型到底决定了什么呢?
用一个实例来进行理解:
%d是整数例如:1,2,3,4
%f是浮点数,例如:1.1,2.0007,1231.45
%c是字符: 就是利用 ASCII 表里面对应的字符。
int main(void)
{
int a = 5;
printf("%d \n",a); // 5
printf("%f \n",a); //-0.012024
}
理解:
printf 过程中,我们的 a 是做右值的,所以我们只关注里面的值。
存储方式:
比如:一个是从前往后排,另一个是从中间往两边排(等等)。
(1)int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);
例如int a = 5;时,编译器给a分配4字节空间,并且将5按照int类型的存储方式转成二进制存到a所对应的内存空间中去(a做左值的)
我们printf去打印a的时候(a此时做右值),printf内部的vsprintf函数会按照格式化字符串(就是printf传参的第一个字符串参数中的%d之类的东西)所代表的类型去解析a所对应的内存空间,解析出的值用来输出。
解析方式
存进去时是按照这个变量本身的数据类型来存储的(譬如本例中a为int所以按照int格式来存储);但是取出来时是按照printf中%d之类的格式化字符串的格式来提取的。
此时虽然a所代表的内存空间中的10101序列并没有变(内存是没被修改的)但是怎么理解(怎么把这些1010转成数字)就不一定了。
譬如我们用%d来解析,那么还是按照int格式解析则值自然还是5;
但是如果用%f来解析,则printf就以为a对应的内存空间中存储的是一个float类型的数,会按照float类型来解析,值自然是很奇怪的一个数字了。
总结:C语言的数据类型的本质
决定这个数在内存当中是怎么进行存储。(也就是1010这些数是怎么存放进去的,int 还是 float,1字节还是4字节)
2、指针数据类型的含义
指针的本质:变量,指针就是指针变量。
int *p;
char *p;
float *p;
(1)对于指针本身来说:
不管什么类型的指针, 他的存储方式都是: 以4字节的形式,依次进行存储。(由32位地址总线决定)
解析方式:都是以地址的方式去解析。
(2)对于指针所指向的那个变量来说:
指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型。
总结:所以我们指针类型是给它所指的变量类型进行准备的,而与它本身无关。
3、 指针类型的强制转换
指针的本质:就是一个地址。在内存当中占用 4个字节来进行存放地址。
我们这里的指针类型的强制转化:是转换 我们所要指向的数据类型。
例如:
int a; 编译器会给内存当中分配 4个字节的空间。
int *p = &a;当我们解引用的时候,int类型占4个字节,指针就从首地址开始移动,读取4个字节。
short *p=&a;当我们解引用的时候,short类型占4个字节,指针就从首地址开始移动,读取2个字节
int main()
{
short c[2]; //等价于申请2个连续的内存空间,每个空间2字节
c[0] = 1; //为第一个short空间赋值为1
c[1] = 1; //为第二个short空间赋值为1
short *p1 = c; //p1指向c[]首地址
int *p2 = (int *)p1; //p2指向c[]首地址,并强制转换类型为 int
printf("p1指向:%p\np2指向:%p\n",p1,p2);
printf("p1取出:%d\np2取出:%d\n",*p1,*p2);
return 0;
}
对应结果为:
p1指向:000000000062FE30
p2指向:000000000062FE30
p1取出:1
p2取出:65537
根据二进制转换得,10000000000000001 为 65537。由此可验证强制转换前指针读取2字节,转化后读取4字节。两个指针指向的首地址相同,但是读出了不同的结果。
具体参考:
https://www.cnblogs.com/al-fajr/p/11615413.html
五、sizeof 运算符(本质和 + - 这样运算符一样)
!!!一定要注意 sizeof 是一个运算符,他不是一个函数,虽然用法很像一个函数。
1、作用:
sizeof 是用来返回()里面 变量 或者 数据类型所占用的字节数。
int a,b;
b = sizeof(a); // b = 4 变量
b = sizeof(int); // b = 4 数据类型
2、sizeof 存在的价值?
主要是因为在不同平台下各种数据类型所占的内存字节数不尽相同(譬如int在32位系统中为4字节,在16位系统中为2字节···)。所以程序中需要使用sizeof来判断当前变量/数据类型在当前环境下占几个字节。
3、sizeof 对数组,字符串(strlen),指针的运算 ,
1、对数组
补充:
1、数组的大小:如果没有给定这个大小,那么他的大小就决定于 初始化的时候他的元素个数。
2、字符串在数组中存放: 在字符串末尾都会自动添加一个 “\0”
3、所以:下面这个 str 的数组的大小为 6个字节。
4、利用 man strlen 可以插这个指令在哪里。
char str[] = "hello";
printf("sizeof(str) = %d.\n", sizeof(str)); // 6
printf("sizeof(str[0]) = %d.\n", sizeof(str[0])); // 1
printf("strlen(str) = %d.\n", strlen(str)); // 5
2、对指针
理解 strlen:
strlen是一个库函数,
strlen的参数:必须是一个字符串(一个指针),即 const char *p ,也就是这个指针指向的变量必须是一个字符串常量。
strlen的返回值:是p往后移动了多少个字节,直到 “\0” 结束
char str[] = "hello";
char *p = str;
printf("sizeof(p) = %d.\n", sizeof(p)); 4 相当于sizeof(char *)
printf("sizeof(*p) = %d.\n", sizeof(*p)); 1 相当于sizeof(char)
printf("strlen(p) = %d.\n", strlen(p)); 5 相当于strlen(str)
再次深入理解:传入的值,是一个const char*p的指针
说明这里这个参数,和他是不是首地址没有关系
char str[] = "hello";
char *p = &str[1];
printf("strlen(p) = %d.\n", strlen(p)); 4
printf("strlen(p) = %d.\n", strlen(&str[2])); 3
3、typedef 和 define 的区别
#include <stdio.h>
#define dpchar char *
typedef char * tpchar; // 用来制造用户自定义类型 ,产生一个新的类型。 (这里必须有 ; 结尾)
int main(void)
{
dpchar p1, p2; // =char *p1,p2; 这样相当于 char *p1, char p2; 这里的* 只能管一个。
tpchar p3, p4; // =char *p3,char *p4; 我们这里的 新类型是同时作用于 p3 和 p4 的。
printf("sizeof(p1) = %d.\n", sizeof(p1)); // 4
printf("sizeof(p2) = %d.\n", sizeof(p2)); // 1
printf("sizeof(p3) = %d.\n", sizeof(p3)); // 4
printf("sizeof(p4) = %d.\n", sizeof(p4)); // 4
return 0;
}
六、指针与函数的传参 (形参做左值,实参做右值)
1、普通变量作为参数的时候 ----- 穿值调用
总结:
1、形参和实参的名字可以不一样。
2、实参和形参的地址也不一样。(分配不同的空间)
实际的本质: 赋值
也就是说,在传参的过程中,我们会新建一个变量(形参),然后将我们传递的变量(实参)赋值给这个新的变量。
void func1(int b)
{
// 在函数内部,形参b的值等于实参a
printf("b = %d.\n", b);
printf("in func1, &b = %p.\n", &b);
}
int main(void)
{
int a = 4;
printf("&a = %p.\n", &a); // &a = 0x7ffc3826c2f4.
func1(a); //b = 4, in func1, &b = 0x7ffc3826c2dc.
return 0;
}
2、数组作为函数的形参 ---- 传址调用
分析:
实参:是一个数组名(不能做左值,做右值表示首元素的地址—相当于指向首元素的指针)
形参:本质就是一个指针。(所以会分配4个字节来存放这个地址)
void fun(char b[100]) 这里这个 [] 里的数可有可无,因为我们直传递了首地址,并不能传递数组大小。
{
return sizeof(b)
}
void main(void)
{
char a[10];
int b;
b=fun(a); b=4;
}
注:我们在传递数组的时候,我们其实是利用了它的右值, a当作右值的时,本质就是首元素的地址。 就是一个指针,指针不管什么情况下,他的大小都是 4字节。
总结:
1、所以我们在传参数组的过程中,我们只能传入它的首地址,我们失去了它的大小。
所以有的函数,在传参的过程中要传入他的大小。
void fun(int *a, sizeof(a))
{
}
3、指针作为函数形参
和数组作为函数形参是一样的.这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的。
void func3(int *a)
{
printf("sizeof(a) = %d.\n", sizeof(a));
printf("in func2, a = %p.\n", a);
}
int main(void)
{
int a[5];
printf("a = %p.\n", a);
func3(a);
return 0;
}
4、结构体变量作为函数形参
回想数组:
数组首地址: 我们只传入了首地址。
整个数组: 我们不传入整个地址。
结构体:
可以看到我们可以访问形参当中的结构体变量。
所以和 int 类型传参过程相似。
缺点:
我们结构体变量很大,所以导致在赋值过程当中,操作效率就会降很多。
解决办法:
我们不传结构体变量,我们穿下面的结构体指针。
struct A
{
char a; // 结构体变量对齐问题
int b; // 因为要对齐存放,所以大小是8
};
void func4(struct A a1)
{
printf("sizeof(a1) = %d.\n", sizeof(a1)); //sizeof(a1) = 8.
printf("&a1 = %p.\n", &a1); //&a1 = 0x7ffc93bee400.
printf("a1.b = %d.\n", a1.b); //a1.b = 5555
}
int main(void)
{
struct A a = linux 内核当中常见的一种赋值方法(中间用逗号隔开)
{
.a = 4,
.b = 5555,
};
printf("sizeof(a) = %d.\n", sizeof(a)); //sizeof(a) = 8.
printf("&a = %p.\n", &a); //&a = 0x7ffc93bee420.
printf("a.b = %d.\n", a.b); //a.b = 5555.
func4(a); //sizeof(a1) = 8.
//&a1 = 0x7ffc93bee400.
//a1.b = 5555.
return 0;
}
5、结构体变量指针作为函数形参
C语言设计的时候,数组传参默认的是首元素首地址。
而我们结构体,C语言没有规定,让我们自己进行选择。
struct A
{
char a; // 结构体变量对齐问题
int b;
};
void func5(struct A *a1)
{
printf("sizeof(a1) = %d.\n", sizeof(a1)); // 8 结构体的大小
printf("sizeof(*a1) = %d.\n", sizeof(*a1)); // 8 指针的大小
printf("&a1 = %p.\n", &a1); // 0x7fffdde01018 ,形参指针的地址
printf("a1 = %p.\n", a1); // 0x7fffdde01030 ,形参指针的内容
printf("a1->b = %d.\n", a1->b); // 通过结构体指针来访问元素
}
int main(void)
{
struct A a =
{
.a = 4,
.b = 5555,
};
printf("sizeof(a) = %d.\n", sizeof(a)); //sizeof(a) = 8.
printf("&a = %p.\n", &a); //&a = 0x7fffdde01030.
printf("a.b = %d.\n", a.b); //a.b = 5555.
func5(&a); // sizeof(a1) = 8. sizeof(*a1) = 8.
//&a1 = 0x7fffdde01018.
//a1 = 0x7fffdde01030. a1->b = 5555.
return 0;
}
七、传值调用和传址调用的经典例子
(1)传值调用描述的是这样一种现象:x和y作为实参,自己并没有真身进入swap1函数内部,而只是拷贝了一份自己的副本(副本具有和自己一样的值,但是是不同的变量)进入子函数swap1,然后我们在子函数swap1中交换的实际是副本而不是x、y真身。所以在swap1内部确实是交换了,但是到外部的x和y根本没有受影响。
(2)在swap2中x和y真的被改变了(但是x和y真身还是没有进入swap2函数内,而是swap2函数内部跑出来把外面的x和y真身改了)。实际上实参x和y永远无法真身进入子函数内部(进去的只能是一份拷贝),但是在swap2我们把x和y的地址传进去给子函数了,于是乎在子函数内可以通过指针解引用方式从函数内部访问到外部的x和y真身,从而改变x和y。
(3)结论:这个世界上根本没有传值和传址这两种方式,C语言本身函数调用时一直是传值的,只不过传的值可以是变量名,也可以是变量的指针。
总结:
实际上我们实参本身永远不可能穿到子函数里面, 我们只能给他传入一个数值。
数值为实参内容时:我们就不可能改变实参本身,只能利用他的值。
数值为实参地址时:我们可以通过对地址的解引用,来间接操控实参本身。
void swap1(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
printf("in swap1, a = %d, b = %d.\n", a, b);
}
void swap2(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
}
int main(void)
{
int x = 3, y = 5;
swap2(&x, &y);
printf("x = %d, y = %d.\n", x, y); // 交换成功
int x = 3, y = 5;
swap1(x, y);
printf("x = %d, y = %d.\n", x, y); // x=3,y=5,交换失败
return 0;
}
八、输入型参数和输出型参数, 函数值
1、函数组成
函数名:函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。
函数体:由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。
形参:是函数的输入部分
返回值:是函数的输出部分。
总结:我们函数的本质就是,对数据(变量)进行加工。怎么做呢?
(1)利用函数传参。(传入地址)
(2)利用全局变量。
2、函数传参时使用 const指针
首先要明确这个 const 是修饰谁?让谁不能修改?
用法:const int *p , 指针变量本身是可变的,指针指向的变量是不可变的。
疑问:既然我们都传入指针(本来目的就是为了间接改变变量的值),为什么不让改变它指向的变量呢?
答:我们加了 const 之后,我们的目的就是不对地址处的内容进行改变。比如字符串。
补充:字符串,指针字符串,数组字符串的区别?
void main(void)
{
char *str = "linux";
char a[] = "uboot";
printf("%p\n",str); // 0x80485d0
printf("%p\n",a); // 0xbfcd1786
printf("%p\n","uboot"); // 0x80485da
}
字符串:
在c语言当中,只要我们使用字符串,那么这个字符串在 代码段 就会被分配空间。
指针字符串:
只是建立了一个指针变量,里面存放着 代码段 字符串的首地址。
数组字符串:
建立一个数组,然后将我们的 代码段处的字符串 赋值给这个数组。
总结:
代码段的内容(字符串常量)这些都是只读的。
只有我们 data 段(数组字符串),才可以进行修改。
#include <stdio.h>
void fun1(const char *p)
{
*p = 'a';
printf("%s\n",p);
}
void main(void)
{
char *str = "linux";
char a[] = "uboot";
printf("%p\n",str);
printf("%p\n",a);
printf("%p\n","uboot");
fun1(str);
}
我们加了const 的情况,在编译阶段就报错
string.c:5:2: error: assignment of read-only location ‘*p’
我们不加 const 的情况,在编译阶段并不会报错, 只是在最后运行的时候就发生了错误
root@ubuntu:/mnt/hgfs/winshare/pointer# gcc string.c
root@ubuntu:/mnt/hgfs/winshare/pointer# ./a.out
0x80485d0
0xbfbc9b96
0x80485da
Segmentation fault (core dumped)
总结:
如果我们形参里面有 const 来修饰,可以提前告诉我们一个信息:
我们这个函数的内部并不会对这个 指针所指向的内容进行修改。
3、函数需要像外部返回多个值时。
需要多个返回值 = 需要修改多个值的本身。
一般来说,返回值只能有一个,而我们需要修改的值有很多个。那怎么办呢?
我们输入和输出都靠传参来实现,返回值只用来表现我们函数是否成功执行。
- 输入型参数:传入普通变量,或者 带 const 的指针。
- 输出型参数:传入指针(地址)。
举例:
输入性参数: 我们利用参数的值,将数带到函数内部。
输出型参数:我们不用它本身的值,只是为了将数从函数内部带出来。
返回值:表示函数执行的结果是否对。 0— 成功, 1 — 表示失败。
int multip(int a, int *p) 当我们看参数,就可以看出很多信息
{
int tmp;
tmp = 5 * a;
if (tmp > 100)
{
return -1; 程序执行错误,(结果 > 100)
}
else
{
*p = tmp; 这一步我们将结果进行返回
return 0; 程序执行成功
}
}
}
int main(void)
{
int a, b = 0, ret = -1;
a = 30;
ret = multip5_3(a, &b);
if (ret == -1)
{
printf("出错了\n");
}
else
{
printf("result = %d.\n", b);
}
return 0;
}
从函数传参,我们可以看出什么呢?
int multip(int a, int *p)
int a; 输入型参数:我们只是利用它的值。
int *p; 输出型参数: 我们用它来接收函数的处理结果。
分析:c语言库中的 strcpy 函数
就是我们为了将,const char *src 进行输入,char *dest 进行输出。
strncpy(char *dest, const char *src, size_t n)
{
size_t i;
for (i = 0; i < n && src[i] != '\0'; i++)
dest[i] = src[i];
for ( ; i < n; i++)
dest[i] = '\0';
return dest;
}