C语言深度解剖 (二)


原文链接: C语言深度解剖 (二)

前言

C语言的水深不见底,好在一些前辈们已经将很多雷区探了一遍

这里分享一下我在学习 《C语言深度解剖》 过程中的一些笔记和心得


概要

  • TOC
    {:toc}


const

准确来说 const 是只读的意思,而不是常量

const 初始目的是为了继承预编译指令的优点而消除它的缺点

Itemconst#define
速度
内存
执行编译过程中编译过程前
类型有类型无类型(替换)
所属变量(只读)常量
void main()
{
	const int a=1;
	int const b=2; //前面两种方式相同
	const int c; //如果在定义时不赋初值,后面将没有机会,因为它是只读的
	int const array[5]={1,2,3,4,5};
	const int brray[5]={1,2,3,4,5};
	int x=1,y=2,z=3,u=4;
	const int *p1=&x; //p1可变,*p1只读
	int const *p2=&y; //p2可变,*p2只读
	int *const p3=&z; //p3只读,*p3可变
	const int *const p4=&u; //p4只读,*p4只读
	//a=3; //error C2166: l-value specifies const object
	//b=4; //error C2166: l-value specifies const object
	//c=5; //error C2166: l-value specifies const object
	//array[0]=6; //error C2166: l-value specifies const object
	//brray[0]=6; //error C2166: l-value specifies const object
	x=7; 
	//*p1=7; //error C2166: l-value specifies const object
	p1=&y;
	y=8;
	//*p2=8; //error C2166: l-value specifies const object
	p2=&x;
	z=9;
	*p3=10;
	//p3=&x; //error C2166: l-value specifies const object
	u=11;
	//*p4=12; //error C2166: l-value specifies const object
	//p4=&x; //error C2166: l-value specifies const object
}

看起来有点混乱,不过这里有一个记忆和理解的方法:

先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近。"近水楼台先得月",离谁近就修饰谁

判断时忽略括号中的类型

  • const (int) *p; //const修饰*p*p是指针指向的对象,不可变
  • (int) const *p; //const修饰*p*p是指针指向的对象,不可变
  • (int)*const p; //const修饰pp不可变,p指向的对象可变
  • const (int) *const p; //前一个const修饰*p,后一个const修饰p,指针pp指向的对象都不可变


volatile

volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象

遇到这个关键字定义的变量,编译器对访问的代码不再进行优化,如何理解这个优化,其实就是为了提升访问速度而不从源地址读取,从缓存中读取,如何理解它的作用,有了这个修饰后可以保证对特殊地址的稳定访问

void main()
{
	volatile const  int i=0;
	const volatile  int j=0;
	//i=2; //error C2166: l-value specifies const object
	//j=3; //error C2166: l-value specifies const object
}

volatileconst 同时修饰时,以 const 的属性为主


union

union 型数据所占的空间等于其最大的成员所占的空间

union 型成员的存取都是相对于该共用体基地址偏移量为0处开始,也就是共用体的访问不论对哪个变量的存取都是从 union 的首地址位置开始的

在小端的存储模式中:

union 
{
	int i;
	char a[2];
}*p,u; //一个 union 会占用四个字节,并且会被初始化为 00000000 00000000 00000000 00000000 ,由于是在全局区,所以会被这样初始化

main()
{
	
	int x;
	p=&u;
	p->a[0]=0x39; //第一个字节被赋为了0x39,于是变成了 00000000 00000000 00000000 00111001
	p->a[1]=0x38; //第二个字节被赋为了0x38,于是变成了 00000000 00000000 00111000 00111001
	x=u.i; //14393,因为是小端模式,低位在前,所以合成结果为14393
}

同样是在小端的存储模式中,如果将共用体定义在main之内,会不会是同样的结果呢?

结果是不一样的

main()
{
	union
	{
		int i;
		char a[2];
	}*p,u; //一个 union 会占用四个字节,并且会被初始化,这是在局部区域,至于初始化为什么值,不同编译器不一样,VC++6.0为 -858993460 即 11001100 11001100 11001100 11001100 ,如果是在全局区,会被初始化为全0
	int x;
	p=&u;
	p->a[0]=0x39; //第一个字节被赋为了0x39,于是变成了 11001100 11001100 11001100 00111001
	p->a[1]=0x38; //第二个字节被赋为了0x38,于是变成了 11001100 11001100 00111000 00111001
	x=u.i; //-859031495,因为是小端模式,低位在前,所以合成结果为 -859031495
}

这个例子说明了以下几点:

  • 同样没有初始化,全局变量和局部变量初值是不一样的,全局为0,局部和编译器还有系统相关
  • 全局变量和局部变量可能并不存在于同一样区域

那这个例子有什么启示呢?

如果要使结果一致,最好给定义的变量赋初值,否则会产生意想不到的结果

存储模式

系统的存储分为两种模式:大端模式小端模式

  • 大端模式(big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中
  • 小端模式(little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中

使用int和char还有共用体的存储特性,可以获取系统的存储模式

小端模式:

int i=1;

00000000 00000000 00000000 00000001

大端模式:

int i=1;

00000001 00000000 00000000 00000000

只要使用char获取起始地址的一个字节,判断是否为1,就可以知道系统是否为小端模式

void main()
{
	union check
	{
		int i;
		char ch;
	}c;
	int a;
	c.i=1;
	a=c.ch; //如果a为1就说明系统是小端模式,如果a为0就说明是大端模式
}

分析下面的输出

#include <stdio.h>

void main()
{
	int a[5]={1,2,3,4,5};
	int *ptr1=(int *)(&a+1); //这个操作是在a地址的基础上跳过5 * sizeof(int) 或者 sizeof(a) 个的地址转化为整型指针后赋给ptr1
	int *ptr2=(int *)((int)a+1); //这个操作是将a地址转化为整型数,然后将值加1,然后将这个整型数转化为整型指针赋给ptr2
	printf("%x,%x\n",ptr1[-1],*ptr2); //将内容以十六进制的形式输出
}

这里有一个以a开头的内容矩阵

00000001 00000000 00000000 00000000
00000010 00000000 00000000 00000000
00000011 00000000 00000000 00000000
00000100 00000000 00000000 00000000
00000101 00000000 00000000 00000000
00000000 00000000 00000000 00000000

ptr1 指向的是 [5][0] ,ptr1[-1] 指向的是 [4][0] 也就是a[4] 与之后的四个字节,也就是 00000101 00000000 00000000 00000000 为 0x5

ptr2 指向的是 [0][1],与之后的四个字节,也就是 00000000 00000000 00000000 00000010 为 0x2000000


enum

如果不赋值则会从被赋初值的那个常量开始依次加1,如果都没有赋值,它们的值从0开始依次递增1

void main()
{
	enum color
	{
		green=2,
		red, //3
		blue=5, 
		yellow, //6
		green_red=10,
		green_blue //11
	}val=11;

	int i=sizeof(red); //4
	int j=sizeof(green); //4
	int k=sizeof(yellow); //4
	int x=sizeof(val); //4
}
`