1.1 K&R C
起初,C语言没有官方标准。1978年由[美国电话电报公司](http://baike.baidu.com/view/190900.htm)(AT&T)贝尔实验室正式发表了C语言。布莱恩·柯林汉(Brian Kernighan) 和 丹尼斯·里奇(Dennis Ritchie) 出版了一本书,名叫《[The C Programming Language](http://baike.baidu.com/view/5012996.htm)》。这本书被 C语言开发者们称为**K&R**,很多年来被当作 C语言的非正式的标准说明。人们称这个版本的 C语言为**K&R C**。
K&R C主要介绍了以下特色:[结构体](http://baike.baidu.com/view/204974.htm)(struct)类型;长整数(long int)类型;无符号整数(unsigned int)类型;把运算符=+和=-改为+=和-=。因为=+和=-会使得编译器不知道使用者要处理i = -10还是i =- 10,使得处理上产生混淆。
即使在后来[ANSI C](http://baike.baidu.com/view/3979609.htm)标准被提出的许多年后,K&R C仍然是许多编译器的最准要求,许多老旧的编译器仍然运行K&R C的标准。
1.2 ANSI C/C89标准
1970到80年代,C语言被广泛应用,从大型主机到小型微机,也衍生了C语言的很多不同版本。1983年,美国国家标准协会(ANSI)成立了一个委员会X3J11,来制定 C语言标准。 1989年,美国国家标准协会(ANSI)通过了C语言标准,被称为**ANSI X3.159-1989 "Programming Language C"**。因为这个标准是1989年通过的,所以一般简称**C89标准**。有些人也简称**ANSI C**,因为这个标准是美国国家标准协会(ANSI)发布的。 1990年,[国际标准化组织](http://baike.baidu.com/view/42488.htm)(ISO)和[国际电工委员会](http://baike.baidu.com/view/159311.htm)(IEC)把C89标准定为C语言的国际标准,命名为**ISO/IEC 9899:1990 - Programming languages -- C[5]** 。因为此标准是在1990年发布的,所以有些人把简称作**C90标准**。不过大多数人依然称之为**C89标准**,因为此标准与ANSI C89标准完全等同。 1994年,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C89标准修订版,名叫ISO/IEC 9899:1990/Cor 1:1994[6] ,有些人简称为**C94标准**。 1995年,国际标准化组织(ISO)和国际电工委员会(IEC)再次发布了C89标准修订版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7] ,有些人简称为**C95标准**。
1.3 C99标准
1999年1月,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C语言的新标准,名叫**ISO/IEC 9899:1999 - Programming languages -- C** ,简称**C99标准**。这是C语言的第二个官方标准。 例如: 增加了新关键字 restrict,inline,_Complex,_Imaginary,_Bool 支持 long long,long double _Complex,float _Complex 这样的类型 支持了不定长的数组。数组的长度就可以用变量了。声明类型的时候呢,就用 int a[*] 这样的写法。不过考虑到效率和实现,这玩意并不是一个新类型。
// 目前大多数编译器都支持了C99标准
2 内存分区
2.1 数据类型
数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。
数据类型可以理解为创建变量的模具: 固定大小内存的别名
2.1.1 数据类型的别名
- typedef的使用
用途:给类型起别名
- 简化struct关键字 ```c / struct Person { char name[64]; //姓名 int age; //年龄 }; typedef struct Person MyPerson; /
typedef struct Person { char name[64]; //姓名 int age; //年龄 }MyPerson;
void test01() { struct Person p1;
MyPerson p2;
}
-
利用typedef区分数据类型
```cpp
void test02()
{
//char* p1, p2;
//char* p3, * p4;
typedef char* PCHAR;
PCHAR p1, p2;
printf("p1的数据类型为:%s\n", typeid(p1).name());
printf("p1的数据类型为:%s\n", typeid(p2).name());
}
提高代码的移植性
typedef long long MYINT; void test03() { MYINT a = 100; MYINT b = 100; }
2.1.2 void数据类型
void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据。
void定义变量是没有任何意义的,当你定义void a,编译器会报错。
void的使用
对函数返回的限定 ```c void func() {
return 10; }
void test02() { func(); printf(“%d\n”, func()); //error 不可以用%d打印void类型数据 }
-
对函数参数的限定
```c
int func(void)
{
return 10;
}
void test02()
{
func(10, 10); //error
}
void *
void * 万能指针
void test03() { printf("sizeof void* = %d\n", sizeof(void*)); //sizeof void* = 4 // void * 称为万能指针 void* p = NULL; int* pInt = NULL; char* pChar = NULL; //pInt = pChar; //从“char *”到“int *”的类型不兼容 pInt = (int *)pChar; // 如果两个不相等的指针类型之间赋值,需要做强转,不报警告提示 pInt = p; //利用万能指针给其他指针赋值的时候,不需要做强转就可以直接赋值 pChar = p; }
2.1.2 sizeof操作符
sizeof是c语言中的一个操作符,类似于++、—等等。sizeof能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。
sizeof的使用
sizeof的本质 —> 操作符
//1.sizeof的本质 // 是不是一个函数??? ==> 不是函数 // 是一个操作符 + - * / void test01() { //如果sizeof里面是一个类型,必须加括号 printf("sizeof int = %d\n", sizeof(int)); //如果统计的是变量,可以不加括号 int a = 10; printf("sizeof int = %d\n", sizeof(a)); printf("sizeof int = %d\n", sizeof a); }
sizeof返回值类型 —> unsigned int
//2.sizeof返回值类型 unsigned int void test02() { //unsigned int a = 10; //if (a - 20 > 0) //当一个unsigned int 和 int 运算时候,先都转为unsigned int //{ // printf("大于0\n"); //} //else //{ // printf("小于0\n"); //} if (sizeof(int) - 5 > 0) { printf("大于0\n"); printf("%u\n", sizeof(int) - 5); } else { printf("小于0\n"); } }
- sizeof统计数组长度 ```c //3.统计数组长度 //当传入函数中,数组名被退化为一个指针,指向数组中第一个元素的地址 void calculateArray(int * arr) { printf(“calculateArray:%d\n”, sizeof(arr)); //calculateArray:4 }
void test03() { int arr[] = { 1,2,3,4,5 };
printf("test03:%d\n", sizeof(arr)); //test03:20
calculateArray(arr);
}
<a name="be020800"></a>
## 2.2 变量
> 既能读又能写的内存对象,称为变量;
> 若一旦初始化后不能修改的对象则称为常量。
-
变量的修改方式
-
直接修改
```c
//1、直接修改
void test01()
{
int a = 10;
printf("%d\n", a);
a = 20;
printf("%d\n", a);
}
- 间接修改
//2、间接修改 void test02() { int a = 10; int* p = &a; printf("a = %d\n", a); *p = 20; printf("a = %d\n", a); }
- 结构体修改 ```c //结构体修改 struct Person { char a; //0~3 int b; //4~7 char c; //8~11 int d; //12~15 };
void test03() { struct Person p = { ‘a’,10,’b’,20 }; printf(“p的d属性的值为:%d\n”, p.d);
p.d = 100;
printf("p的d属性的值为:%d\n", p.d);
struct Person* pp = &p;
pp->d = 1000;
printf("p的d属性的值为:%d\n", p.d);
}
-
地址偏移修改
```c
void test04()
{
struct Person p = { 'a',10,'b',20 };
char* pp = &p;
printf("sizeof struct Person = %d\n", sizeof(struct Person)); //16
*(int*)(pp + 12) = 1000;
printf("%d\n", pp);
printf("%d\n", pp + 1);
printf("p的d属性的值为:%d\n", *(int *)(pp + 12)); //p的d属性的值为:1000
printf("p的d属性的值为:%d\n", *(int*)((int*)pp + 3));
}
2.3 程序的内存分区模型
2.3.1 运行之前
我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译
1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:将目标文件链接为可执行程序
当我们编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:
![](assets%5CQQ%E5%9B%BE%E7%89%8720200101190240.png#alt=)
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
代码区
存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
# 总体来讲说,程序源代码被编译之后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据域段和.bss段属于程序数据。
那为什么把程序的指令和程序数据分开呢?
- 程序被load到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改;
- 当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer 7.0运行起来之后, 它需要占用112 844KB的内存,它的私有部分数据有大概15 944KB,也就是说有96 900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。
2.3.2 运行之后
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
2.3.3 分区模型
- 栈区
由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。
- 栈区注意事项:不要返回局部变量的地址 ```c //不要返回局部变量的地址 int * func() { int a = 10; //局部变量 存放在栈区 return &a; }
void test01() { int p = func(); //下面的结果不重要,非法操作这块内存 printf(“p = %d\n”, p); printf(“p = %d\n”, *p); }
char* getString() { char str[] = “hello world”;
return str; }
void test02() { char* p = NULL;
p = getString();
//getString中的str已经被释放了,再通过指针访问,也属于非法访问 printf(“%s\n”, p); }
-
堆区
> 由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
-
堆区的使用
```c
int* getSpace()
{
int* p = malloc(sizeof(int) * 5);
if (p == NULL)
{
printf("分配内存失败\n");
return;
}
for (int i = 0; i < 5; i++)
{
p[i] = 100 + i;
}
return p;
}
void test01()
{
int* p = getSpace();
for (int i = 0; i < 5; i++)
{
printf("%d\n", p[i]);
}
//堆区数据手动开辟,手动释放
//释放堆区数据
if (p != NULL)
{
free(p);
p = NULL;
}
}
堆区注意事项 ```c void allocateSpace(char pp) { char temp = malloc(100); memset(temp, 0, 100);
strcpy(temp, “hello world”);
pp = temp;
free(temp); }
void test02() { char* p = NULL;
//如果主调函数中指针为NULL,被调函数用同级指针是不会修饰主调函数中的指针的
allocateSpace(p);
printf("%s\n", p); // (null)
}
-
堆区解决方式
-
通过高级指针修饰低级指针
```c
//解决方式1 利用高级指针 修饰低级指针
void allocateSpace2(char** pp)
{
char * temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
*pp = temp;
}
void test03()
{
char* p = NULL;
allocateSpace2(&p);
printf("%s\n", p);
if(p!=NULL)
{
free(p);
p = NULL;
}
}
-
通过函数返回值
//解决方式2 通过返回值
char* allocateSpace3()
{
char* temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
return temp;
}
void test04()
{
char* p = NULL;
p = allocateSpace3();
printf("%s\n", p);
if (p != NULL)
{
free(p);
p = NULL;
}
}
堆分配内存API
参数: nmemb:所需内存单元数量 size:每个内存单元的大小(单位:字节) 返回值: 成功:分配空间的起始地址 失败:NULL */
```c
//calloc
//calloc和malloc相同点是在堆区分配内存
//calloc和malloc不同点是calloc会清空内存,而malloc不会
void test01()
{
//malloc不会清空内存
//int* p = malloc(sizeof(int) * 10);
//calloc会清空内存为0
int* p = calloc(10, sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
if (p != NULL)
{
free(p);
p = NULL;
}
}
-
realloc
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
/*
功能:
重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。
参数:
ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
size:为重新分配内存的大小, 单位:字节
返回值:
成功:新分配的堆内存地址
失败:NULL
*/
//realloc
void test02()
{
int* p = malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
{
p[i] = 100 + i;
}
for (int i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
printf("%d\n", p);
//重新分配内存 分配20个int大小,新空间的数据不会清空
p = realloc(p, sizeof(int) * 20);
printf("%d\n", p);
for (int i = 0; i < 20; i++)
{
printf("%d\n", p[i]);
}
if (p != NULL)
{
p = NULL;
free(p);
}
}
- 全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储**全局变量**、**静态变量**和**常量**。
- 全局变量和静态变量 ```c // test.c extern int g_a = 1000; //全局变量 默认c语言中 前面加了一个关键字extern,属于外部链接属性
static int s_a = 10; //只能在本文件中使用
```c
//1、全局变量
//声明时候写在函数体外部
//在本文件或其他文件中都可以使用
void test01()
{
extern int g_a; //告诉编译器g_a在别的文件中,下面使用的时候不要报错
printf("g_a = %d\n", g_a);
}
//2、静态变量 只能在本文件中使用,内部链接属性
void test02()
{
//extern int s_a;
//printf("s_a = %d\n",s_a); error
//局部静态变量,函数外无法直接使用,但是可以做为返回值被外部使用
static int s_b = 1000; //只会被初始化一次
}
常量
- const修饰的常量 ```c //1、const修饰的变量 称为常量
//const修饰的全局常量 const int a = 10; // 存放在常量区,受到常量区的保护
void test01() { //a = 1000; //error 直接修改失败
int* p = &a;
*p = 1000; //间接修改语法通过,运行失败
printf("%d\n", a);
}
//const修饰的局部常量 void test02() { const int b = 10; //b放在栈上,直接修改失败,间接修改成功,因此在c语言中,const修饰的局 部常量称为伪常量
//a = 1000; //error 直接修改失败
int* p = &b;
*p = 1000;
printf("%d\n", b);
//int arr[b]; //error 伪常量 不可以初始化数组
}
-
字符串常量
```c
//字符串常量 共享的?
void test03()
{
char* p1 = "hello world";
char* p2 = "hello world";
char* p3 = "hello world";
printf("%d\n", p1); //1801180
printf("%d\n", p1); //1801180
printf("%d\n", p1); //1801180
printf("%d\n", &"hello world"); //1801180
}
//字符串常量 只读的?
void test04()
{
char* p1 = "hello world";
printf("%c\n", p1[0]);
//p1[0] = 'w'; //error vs中是只读的,是不可以修改的
}
字符串常量是否可修改?字符串常量优化:
ANSI C中规定:修改字符串常量,结果是未定义的。 ANSI C并没有规定编译器的实现者对字符串的处理,例如:
有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。
有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。
所以尽量不要去修改字符串常量
C99标准:
char p = “abc”; defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. *If an attempt is made to use p to modify the contents of the array, the behavior is undefined
字符串常量地址是否相同?
tc2.0,同文件字符串常量地址不同
Vs2013/2019,字符串常量地址同文件和不同文件都相同.
Dev c++、QT同文件相同,不同文件不同。
3 总结
在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。
数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。
可以说,C/C++内存分区其实只有两个,即代码区和数据区