【C高级专题】指针才是C语言的精髓2

本文深入剖析了C语言中指针与数组、函数传参的关系,详细解释了sizeof运算符的应用,以及指针作为输入型和输出型参数时的注意事项。讨论了野指针问题及其防范措施,并介绍了const关键字修饰指针的四种形式,强调了const在编程中的信息传递作用。
摘要由CSDN通过智能技术生成

目录

章节介绍

1.指针、数组与sizeof运算符
本节依次讲解了一些和指针、数组有关的sizeof运算的案例,通过这些代码实例希望让大家深入掌握一些C语言编程细节的语法表现,从而对指针和数组有更深入的理解。
2.指针与函数传参
本节讲解数组、指针、结构体等类型变量作为函数参数的情况。通过示例演示和分析让大家明白函数传参的实质,传值调用和传址调用等概念的真实内涵。
3.输入型参数与输出型参数
本节再次讲解函数传参,const修饰指针变量等知识点,最后引入输入型参数和输出型参数的概念。
4.野指针问题
本节首先介绍什么是野指针以及野指针造成的原因和后果,然后重点讲述如何防止野指针的出现,希望大家能养成良好的使用指针的习惯,避免出现野指针导致程序运行时错误。
5.const关键字与指针
本节讲述const关键字的用法,重点是const关键字修饰指针的4种常见格式,及其理解方法。通过本节学习希望大家掌握const关键字在修饰指针时的用法。

一、指针、数组与sizeof运算符

(1)sizeof是C语言的一个运算符(主要sizeof不是函数,虽然用法很像函数),sizeof的作用是用来返回()里面的变量或者数据类型占用的内存字节数,返回的值都是以字节为单位。
(2)sizeof存在的价值?主要是因为在不同平台下各种数据类型所占的内存字节数不尽相同(譬如int在32位系统中为4字节,在16位系统中为2字节···)。所以程序中需要使用sizeof来判断当前变量/数据类型在当前环境下占几个字节。

1、字符数组与sizeof

	char str[] = "hello";  
	printf("sizeof(str) = %d.\n", sizeof(str));				// 6 数组str在内存占用的字节空间,包含字符串结尾的'\0'
	printf("sizeof(str[0]) = %d.\n", sizeof(str[0]));		// 1	数组的第一个元素在内存中占用的字节数 数组是char类型的。
	printf("strlen(str) = %d.\n", strlen(str));				// 5
	// 函数传递数组名其实等价与传递指针 相当与数组首元素的首地址&a[0], 
	// strlen函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符’\0’停止。返回的长度大小不包括‘\0’。

2、指针、字符数组与sizeof

	char str[] = "Hello";
	char *p = str;			// p指向数组首元素的首地址
	printf("sizeof(p) = %d.\n", sizeof(p)); 	// p表示指针变量自己本身,相当于(char *) 占4字节
	printf("sizeof(*p) = %d.\n", sizeof(*p));	// p是char *,p所指向的变量是char,p是指向&str[0]的指针,*p解引用的类型是char
	printf("sizeof(str[0]) = %d.\n", sizeof(str[0]));	// 等价
	printf("strlen(p) = %d.\n", strlen(p));	

(1)32位系统中所有指针的长度都是4,64位系统中所有的指针的长度都是8,不管是什么类型的指针。
(2)strlen是一个C库函数,用来返回一个字符串的长度(注意,字符串的长度是不计算字符串末尾的’\0’的)。一定要注意strlen接收的参数必须是一个字符串(字符串的特征是以’\0’结尾)
参数必须是字符型指针(char *)。当数组名作为参数传入时,实际上数组就退化成指针了。
该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符’\0’停止。返回的长度大小不包括‘\0’。

3、

	int a = 10;
	printf("sizeof(a) = %d.\n", sizeof(a)); 		// 4
	printf("sizeof(int ) = %d.\n", sizeof(int ));	// 4

(1)sizeof测试一个变量本身,和sizeof测试这个变量的类型,结果是一样的。

4、数组与sizeof

	int b1[100] = {0};
	printf("sizeof(b1) = %d.\n", sizeof(b1));   // 100×sizeof(int)
												// 数组个数 * 每个数据在内存中占用的字节数

(1)sizeof(数组名)的时候,数组名不做左值也不做右值,纯粹就是数组名的含义。那么sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)。

5、函数传参与sizeof

// func完全等同于func1
void func(int a[])
{
	printf("数组大小 = %d.\n", sizeof(a));		// 4
}

void func1(int *a)
{
	printf("数组大小 = %d.\n", sizeof(a));		// 4
}

void func2(int *a, int num)
{
	// 在子函数内,a是传进来的数组的指针(首地址)
	// 在子函数内,num是数组的大小
}
int main (void)
{
	int a[20];
	
	func(a);	// 4 因为传参的a做右值,相当与数组首元素的首地址,a在函数func内部就是指针,而不是数组
	func2(a, sizeof(a));
	
	return 0;
}

(1)函数传参,形参是可以用数组的
(2)函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。

6、类型重定义

#define dpChar char *
typedef char * tpChar;		// typedef用来重命名类型,或者说用来制造用户自定义类型

int main (void)
{
	dpChar p1,  p2;			// 展开:char *p1, p2; 相当于char *p1, char p2;
	tpChar p3,  p4;			// 等价于:char *p3, char *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
}
*/

7、数组元素个数

	int a[56];
	int b = sizeof(a) / sizeof(a[0]);	// 整个数组字节数/数组中一个元素的字节数
	printf("b = %d.\n", b);				// 结果应该是数组的元素个数
	

二、 指针与函数传参

1、普通变量作为函数形参

void fun(int b)
{
	printf("a = %d.\n", b);		// 5
	printf("b = %p.\n", &b);		// &a != &b
}
// &a和&b不同,说明a和b不是同一个变量(在内存中a和b是独立的2个内存空间)
// 但是a和b是有关联的,实际上b是a赋值得到的。
int mian(void)
{
	int a = 5;
	printf("a = %p.\n", &a);
	fun(a);
	
	return 0;
}

(1)函数传参时,普通变量作为参数时,形参和实参名字可以相同也可以不同,实际上都是用实参来替代相对应的形参的。
(2)在子函数内部,形参的值等于实参。原因是函数调用时把实参的值赋值给了形参。
(3)“传值调用”(相当于实参做右值,形参做左值)函数调用的过程,其实就是实参传递给形参的一个过程。这个传递实际是一次拷贝。实际参数的时候,实参(本质是一个变量)本身并没有进入到函数内,而是把自己的值复制了一份传给了函数中的形参,在函数中参与运算。这种传参方法,就叫做传值调用。

2、数组作为函数形参

void func3(int *a)
{
	printf("sizeof(a) = %d.\n", sizeof(a));		// 4
	printf("sizeof(*a) = %d.\n", sizeof(*a));	// 4	a是int *类型,*a是int类型
	printf("in func2, a = %p.\n", a);
}

void func2(int a[])
{
	printf("sizeof(a) = %d.\n", sizeof(a));		// 4
	printf("in func2, a = %p.\n", a);
}

int mian(void)
{
	int a[] = {0};
	printf("a = %p.\n", a);
	func2(a);		// 传数组名在函数中是做右值,传递的是数组首元素的首地址
	
	return 0;
}

(1)数组名作为形参传参时,实际传递是不是整个数组,而是数组的首元素的首地址(也就是整个数组的首地址。因为传参时是传值,地址作为一个数字传,而不是传意义,所以这两个没区别)。所以在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针。所以sizeof得到的是4.
(2)在子函数内传参得到的数组首元素首地址,和外面得到的数组首元素首地址的值是相同的。很多人把这种特性叫做“传址调用”(所谓的传址调用就是调用子函数时传了地址(也就是指针),此时可以通过传进去的地址来访问实参。)
(3)数组作为函数形参时,[]里的数字是可有可无的。为什么?因为数组名做形参传递的实际只是个指针,根本没有数组长度这个信息。

(1)第二种功能是指针解引用,解引用时*p表示p指向的变量本身
(2)一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量指向的那个变量
(3)int *p;定义指针变量时,p(指针变量本身)是int *类型,*p(指针指向的那个变量)是int类型的。 (int )4
(4)int
类型说白了就是指针类型,所有的指针变量都是指针类型,只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)的。结论就是:*所有的指针类型(不管是int * 还是char * 还是double )的解析方式是相同的,都是地址(指针存放的是某一个变量的地址,占用4个字节)。
(5)对于指针所指向的那个变量来说,指针的类型就很重要了。指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型。譬如指针是
int *的,那么指针所指向的变量就是int类型的。

4.3.9.3、指针作为函数形参

// 指针变量也是变量,在func2函数中int *a也是局部变量,
// 对指针变量进行运算是没有意义的,局部变量的生命周期
void func2(int *a)			// 在函数中可以*a改变p所指向变量的值
{
	printf("sizeof(a) = %d.\n", sizeof(a));		// 4
	printf("in func2, a = %p.\n", a);
	*a = 10;
}

int main(void)
{
	int a = 5;
	int *p = &a;
	printf("sizeof(p) = %d.\n", sizeof(p));		// 4
	printf("in func2, p = %p.\n", p);			// 地址值
	printf("*p = %d.\n", *p);					// 5
	func2(p);									// 传递的变量本身的地址值,
	printf("*p = %d.\n", *p);					// 10
}

(1)只有一句话:和数组作为函数形参是一样的.这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的。

4、结构体变量作为函数形参

struct A
{
	char a;				// 结构体变量对齐问题
	int b;				// 因为要对齐存放,所以大小是8
};

void func5(struct A *a1)		// 传递的是结构体变量的指针
{
	printf("sizeof(a1) = %d.\n", sizeof(a1));		// 4
	printf("sizeof(*a1) = %d.\n", sizeof(*a1));		// 8	struct A类型
	printf("sizeof(a1->b) = %d.\n", sizeof(a1->b));	// 4	int类型
	printf("&a1 = %p.\n", &a1);			// 二重指针
	printf("a1 = %p.\n", a1);			// a1的地址
	printf("a1->b = %d.\n", a1->b);		// 当函数传递的是结构体变量指针的时候,访问结构体变量的元素用->运算符
}

void func4(struct A a1)
{
	printf("sizeof(a1) = %d.\n", sizeof(a1));
	printf("&a1 = %p.\n", &a1);
	printf("a1.b = %d.\n", a1.b);
}
int main(void)
{
/*
	struct A a = 
	{
		.a = 4,
		.b = 5555,
	};
	printf("sizeof(a) = %d.\n", sizeof(a));		// 4
	printf("&a = %p.\n", &a);
	printf("a.b = %d.\n", a.b);
	func5(&a);					// 传递的是结构体变量的指针,所有需要用传递结构体变量的地址
*/
	
/*
	struct A a = 
	{
		.a = 4,
		.b = 5555,
	};
	printf("sizeof(a) = %d.\n", sizeof(a));
	printf("&a = %p.\n", &a);
	printf("a.b = %d.\n", a.b);
	func4(a);
*/
	return 0;
}

(1)结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的。所以说结构体变量其实也是普通变量而已。
(2)因为结构体一般都很大,所以如果直接用结构体变量进行传参,那么函数调用效率就会很低。(因为在函数传参的时候需要将实参赋值给形参,所以当传参的变量越大调用效率就会越低)。怎么解决?思路只有一个那就是不要传变量了,改传变量的指针(地址)进去。
(3)结构体因为自身太大,所以传参应该用指针来传(但是程序员可以自己决定,你非要传结构体变量过去C语言也是允许的,只是效率低了);回想一下数组,为什么C语言设计的时候数组传参默认是传的数组首元素首地址而不是整个数组?

5、传值调用与传址调用

// 传值,传的是x,y的一份复制的数值。
// 形参的生命周期是从该函数开始到以 } 结束
void swap1(int a, int b)
{
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
	printf("in swap1, a = %d, b = %d.\n", a, b);
}
/* 实参x和y永远无法真身进入子函数内部(进去的只能是一份拷贝),
在swap2我们把x和y的地址传进去给子函数了,于是乎在子函数内可
以通过指针解引用方式从函数内部访问到外部的x和y真身,从而改变x和y*/
//  传值,传的是x,y的地址值。
void swap2(int *a, int *b)
{
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
	printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
}
/* 传参是a,b的地址,都是形参,在函数改变形参就相当于在普通变量
在函数中改变值是一样的,并不影响原来的x,y地址,地址运算是毫无意义的*/
// 在函数内部确实是交换了a,b的地址值,所以打印出来的值交换了
// 在函数改变指针的指向(地址值),就必须传递指针的指针,或return ?
void swap3(int *a, int *b)
{
	int *tmp;
	tmp = a;	
	a = b;
	b = tmp;
	
	printf("tmp = %p.\n", tmp);
	printf("a = %p.\n", a);
	printf("b = %p.\n", b);
	printf("in swap2, *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)传值调用描述的是这样一种现象: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语言本身函数调用时一直是传值的,只不过传的值可以是变量名,也可以是变量的指针。
如果希望变量本身被改变,传指针然后通过在函数内部解引用的方式访问外部真身。

三、输入型参数与输出型参数

1、函数为什么需要形参与返回值
(1)函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。
(2)函数体是函数的关键,由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。
(3)形参列表和返回值。形参是函数的输入部分,返回值是函数的输出部分。对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器),形参列表就是这个机器的原材料输入端;而返回值就是机器的成品输出端。
(4)其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量。
(5)全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些;但是实战中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。

2、函数传参中使用const指针

// char *pStr = "linux";调用func3();assignment of read-only location '*p' 
void func3(char *p)				// 与char pStr[] = "linux";匹配
{
	*p = 'a';
}
// char pStr[] = "linux";调用func4();段错误
void func4(const char *p)		// 与char *pStr = "linux";匹配
{
	*p = 'a';
}
int main(void)
{
	char *pStr = "linux";		// 段错误  原因是在内存存储的位置不一样	存放Date段,不可变的
	//char pStr[] = "linux";	// ok的
	func3(pStr);
	printf("%s.\n", pStr);
	
	return 0;
}

(1)const一般用在函数参数列表中,用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的)。
(2)const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容,所以给该函数传一个不可改变的指针(char *p = “linux”;这种)不会触发错误,const起限定函数的作用,而一个未声明为const的指针的函数,你给他传一个不可更改的指针的时候就要小心了。触发段错误(核心已转储)(core dumped)问题的分析方法

3、函数需要向外部返回多个值时怎么办?
(1)一般来说,函数的收入部分就是函数参数,输出部分就是返回值。问题是函数的参数可以有很多个,而返回值只能有1个。这就造成我们无法让一个函数返回多个值。
(2)现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回(在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败)
(3)普遍做法,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)。如果这个参数是用来做输入的,就叫输入型参数;如果这个参数的目的是用来做输出的,就叫输出型参数。
(4)输出型参数就是用来让函数内部把数据输出到函数外部的。

// 返回值用来表示函数执行的结果是对(成功)还是错(失败)
// 通过参数做返回值
int multip5_3(int a, int *p)
{
	int tmp;

	tmp = 5 * a;
	if (tmp > 100)
	{
		return -1;
	}
	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);
	}
}

4、总结
(1)看到一个函数的原型后,怎么样一眼看出来哪个参数做输入哪个做输出?函数传参如果传的是普通变量(不是指针)那肯定是输入型参数;如果传指针就有2种可能性了,为了区别,经常的做法是:如果这个参数是做输入的(通常做输入的在函数内部只需要读取这个参数而不会需要更改它)就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的。
譬如C库函数中strcpy函数

char * strcpy(char *dst,const char *src)   
{
    assert(dst != NULL && src != NULL);   // assert()断言函数 
    char *ret = dst; 					  // dst数组首元素的首地址
 
    while ((*dst++ = *src++)!='\0'); 	 // strcpy字符串
 
    return ret;
}

四、 野指针问题

1、神马是野指针?哪里来的?有什么危害?
(1)野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(2)野指针很可能触发运行时段错误(Sgmentation fault)
(3)因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。
(4)野指针因为指向地址是不可预知的,所以有3种情况
第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;
第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;
第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。
(5)指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值),就决定了栈的使用多少会影响这个默认值。因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义。因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。

2、怎么避免野指针?
(1)野指针的错误来源就是指针定义了以后没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。
(2)知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间
(3)常规的做法是:
第一点:定义指针时,同时初始化为NULL
第二点:在指针解引用之前,先去判断这个指针是不是NULL
第三点:指针使用完之后,将其赋值为NULL
第四点:在指针使用之前,将其赋值绑定给一个可用地址空间
(4)野指针的防治方案4点绝对可行,但是略显麻烦。很多人懒得这么做,那实践中怎么处理?在中小型程序中,自己水平可以把握的情况下,不必严格参照这个标准;但是在大型程序,或者自己水平感觉不好把握时,建议严格参照这个方法。

3、NULL到底是什么?
(1)NULL在C/C++中定义为:

// 在C++和C中定义NULL 0 和 (void *)0 主要是C需要做严格的类型检查
#ifdef _cplusplus			// 定义这个符号就表示当前是C++环境
#define NULL 0				// 在C++中NULL就是0
#else
#define NULL (void *)0		// 在C中NULL是强制类型转换为void *的0
#endif

(2)在C语言中,int *p;你可以p = (int *)0;但是不可以p = 0;因为类型不相同。
(3)所以NULL的实质其实就是0,然后我们给指针赋初值为NULL,其实就是让指针指向0地址处。为什么指向0地址处?2个原因。第一层原因是0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针)第二层原因是这个地址0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果了。
(4)一般在判断指针是否野指针时,都写成
if (NULL != p)
而不是写成 if (p != NULL)
原因是:如果NULL写在后面,当中间是号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来;如果习惯了把NULL写在前面,当错误的把==写成了=时,编译器会报错,程序员会发现这个错误。

C语言的笔试题

输出结果:100?0?段错误退出?哪一句导致的?为什么?


#include <stdio.h>

/* 在指针作为函数参数传参时,改变形参就相当于在普通变量作为函数参数传参时改变值是一样的,只限制与该函数内部有效*/
void fun(int *node)
{
    static int N = 100;
    node = &N;
}
int main()
{
    int *node = NULL;
    int a = 0;
    
    fun(node);
    a = *node;			// 化简 :a = *NULL; 引发段错误
    
    printf("%d.\n", a);
    
    return 0;
   }

五、 const关键字与指针

1、const修饰指针的4种形式
(1)const关键字,在C语言中用来修饰变量,表示这个变量是常量。
(2)const修饰指针有4种形式,区分清楚这4种即可全部理解const和指针。

	第一种:const int *p;		// p本身不是const的,p所指向的变量是const的
	第二种:int const *p;		// p本身不是const的,p所指向的变量是const的
	第三种:int * const p;		// p本身是const的,p指向的变量不是const的
	第四种:const int * const p;	// p本身是const的,p指向的变量也是const

(3)*关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p本身,第二个是p指向的那个变量(p)。一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const放在某个位置是修饰谁的

2、const修饰的变量真的不能改吗?
(1)课堂练习说明:const修饰的变量其实是可以改的(前提是gcc环境下)。
(2)在某些单片机环境下,const修饰的变量是不可以改的。const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。
(3)在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
(4)更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。
(5)gcc把const类型的常量也放在了data段,const是通过编译器在编译的时候执行检查来确保实现的

3、const究竟应该怎么用
(1)const是在编译器中实现的,编译时检查,并非不能骗过(强制类型转换)。所以在C语言中使用const,就好象是 一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的
信息的一种传递

4、总结
const往后看,紧挨着指针变量,则修饰指针变量本身
const往后看,没有紧挨着指针变量,则修饰指针所指向变量

注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tyx-☆、、、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值