大家好,我是小林,推荐朋友阿糖的一篇文章。 C语言概述
欢迎来到 C 语言的世界,这是一种强大而专业的编程语言。
1.1 C语言的起源
贝尔实验室在1972年开发了C,当时他正在和ken一起设计UNIX操作系统,但是,C并没有完全构思出来。 它来自B语言。
1.2 使用C语言的原因
在过去的几十年里,C 语言已经成为最流行和最重要的编程语言之一。 它的成长是因为人们在尝试时喜欢它。 这些年来,许多人已经从 C 转向更强大的 C++ 语言,但 C 有其自身的优势,仍然是一种重要的语言,并且是学习 C++ 的唯一途径。
1.3 C语言标准
1.3.1 K&R C
起初,C 语言没有官方标准。 C 语言于 1978 年由 AT&T 贝尔实验室正式出版。布赖恩·科林厄姆 (Brian) 和丹尼斯·里奇 ( ) 出版了一本名为 The C 的书。 这本书被C语言开发者称为K&R,多年来一直作为C语言的非官方标准规范。 人们称这个版本的 C 语言为 K&R C。
K&R C 主要介绍了以下特性: () 类型; 长整数(long int)类型; 无符号整数 (int) 类型; 将运算符 =+ 和 =- 更改为 += 和 -=。 因为=+和=-会让编译器不知道用户应该处理i=-10还是i=-10,从而造成处理混乱。
即使在 ANSI C 标准提出多年后,K&R C 仍然是很多编译器最准确的要求,很多老的编译器仍然运行 K&R C 标准。
1.3.2 ANSI C/C89标准
从 70 年代到 80 年代,C 语言被广泛使用。 从大型机到小型微型计算机,还衍生出了许多不同版本的C语言。 1983 年,美国国家标准协会 (ANSI) 成立了 X3J11 委员会来制定 C 语言的标准。
1989 年,美国国家标准协会 (ANSI) 采用了 C 语言的标准,称为 ANSI X3.159-1989“C”。 因为这个标准是在1989年通过的,所以一般被称为C89标准。 有些人还将其简称为 ANSI C,因为该标准是由美国国家标准协会 (ANSI) 发布的。
1990年,国际标准化组织(ISO)和国际电工委员会(IEC)将C89标准定义为C语言的国际标准,命名为ISO/IEC 9899:1990--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[7],有人称之为简称C95标准。
1.3.3 C99标准
1999年1月,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C语言的新标准,称为ISO/IEC 9899:1999-C,简称C99标准。 这是 C 语言的第二个官方标准。
例如:
2. 内存分区 2.1 数据类型
2.1.1 数据类型概念
什么是数据类型? 为什么需要数据类型? 数据类型是为了更好的内存管理,让编译器可以决定分配多少内存。
在我们现实生活中,狗就是狗,鸟就是鸟等等,万物都有自己的类型,所以程序中用到的数据类型也来源于生活。
当我们为狗分配内存时,相当于给狗建了一个狗窝,而当我们为鸟分配内存时,就相当于为鸟建了一个鸟巢。 我们可以给他们每个人建一个别墅,但是会造成内存浪费。 ,不能很好的利用内存空间。
我们在想,如果给鸟分配内存,我们只需要一个鸟巢大小的空间,如果给狗分配内存,我们只需要一个狗窝大小的内存,而不是给两只鸟都分配一个别墅和狗,造成内存浪费。
当我们定义一个变量,a = 10,编译器是如何分配内存的? 电脑只是一台机器,它怎么知道自己能装多少内存10?
因此,数据类型非常重要,它可以告诉编译器分配多少内存来存放我们的数据。
狗窝里有狗,鸟舍里有鸟。 如果没有数据类型,你怎么知道冰箱里有大象!
数据类型的基本概念:
2.1.2 数据类型别名
typedef unsigned int u32;
typedef struct _PERSON{
char name[64];
int age;
}Person;
void test(){
u32 val; //相当于 unsigned int val;
Person person; //相当于 struct PERSON person;
}
2.1.3 void数据类型
void字面意思是“无类型”,void*无类型指针,无类型指针可以指向任何类型的数据。
用void定义一个变量是没有意义的。 当你定义void a时,编译器会报错。
void 实际用于以下两种方式:
//1. void修饰函数参数和函数返回
void test01(void){
printf("hello world");
}
//2. 不能定义void类型变量
void test02(){
void val; //报错
}
//3. void* 可以指向任何类型的数据,被称为万能指针
void test03(){
int a = 10;
void* p = NULL;
p = &a;
printf("a:%d\n",*(int*)p);
char c = 'a';
p = &c;
printf("c:%c\n",*(char*)p);
}
//4. void* 常用于数据类型的封装
void test04(){
//void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}
2.1.4 运营商
它是C语言中的一个运算符,类似于++、--等。 它可以告诉我们编译器在为某个数据或某类数据分配内存空间时分配的大小,大小以字节为单位。
基本语法:
sizeof(变量);
sizeof 变量;
sizeof(类型);
当心:
//1. sizeof基本用法
void test01(){
int a = 10;
printf("len:%d\n", sizeof(a));
printf("len:%d\n", sizeof(int));
printf("len:%d\n", sizeof a);
}
//2. sizeof 结果类型
void test02(){
unsigned int a = 10;
if (a - 11 < 0){
printf("结果小于0\n");
}
else{
printf("结果大于0\n");
}
int b = 5;
if (sizeof(b) - 10 < 0){
printf("结果小于0\n");
}
else{
printf("结果大于0\n");
}
}
//3. sizeof 碰到数组
void TestArray(int arr[]){
printf("TestArray arr size:%d\n",sizeof(arr));
}
void test03(){
int arr[] = { 10, 20, 30, 40, 50 };
printf("array size: %d\n",sizeof(arr));
//数组名在某些情况下等价于指针
int* pArr = arr;
printf("arr[2]:%d\n",pArr[2]);
printf("array size: %d\n", sizeof(pArr));
//数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小
TestArray(arr);
}
2.1.5 数据类型总结
2.2 变量
2.1.1 变量的概念
可读写的内存对象称为变量;
一旦初始化就不能修改的对象称为常量。
变量定义形式: 类型 标识符, 标识符, … , 标识符
2.1.2 变量名的性质
修改变量有两种方式:
void test(){
int a = 10;
//1. 直接修改
a = 20;
printf("直接修改,a:%d\n",a);
//2. 间接修改
int* p = &a;
*p = 30;
printf("间接修改,a:%d\n", a);
}
2.3 程序内存分区模型
2.3.1 内存分区
2.3.1.1 运行前
如果我们要执行我们写的c程序,第一步就是编译程序。 1)预处理:宏定义展开,头文件展开,条件编译,这里不检查语法
2)编译:检查语法,将预处理后的文件编译生成汇编文件
3):将汇编文件生成成目标文件(二进制文件)
4)链接:将目标文件链接成可执行程序
代码区
存放CPU执行的机器指令。 通常代码区是共享的(即另一个可执行程序可以调用它),共享的目的是对于频繁执行的程序,内存中只需要一份代码。 代码区通常是只读的,将其设置为只读的原因是为了防止程序意外修改其指令。 另外,代码区还规划了局部变量的相关信息。
全局初始化数据区/静态数据区(数据段)
该区域包含程序中显式初始化的全局变量、已经初始化的静态变量(包括全局静态变量和t)和常量数据(如字符串常量)。
未初始化数据区(也叫bss区)
存储的是全局未初始化的变量和未初始化的静态变量。 未初始化数据区中的数据在程序开始执行前被内核初始化为0或空(NULL)。
一般来说,程序源代码编译后主要分为程序指令(代码区)和程序数据(数据区)两段。 代码段属于程序指令,数据域段和.bss段属于程序数据。
那么为什么要将程序指令与程序数据分开呢?
2.3.1.1 运行后
在程序加载到内存之前,代码区和全局区(data和bss)的大小是固定的,在程序运行过程中不能改变。 然后,当可执行程序运行时,操作系统将物理硬盘程序加载(loads)到内存中,除了代码区(text)、数据区(data)和未初始化数据区(bss)按照添加了可执行程序的信息,以及额外的栈区和堆区。
代码区(文字)
加载的是可执行文件的代码段,所有的可执行代码都加载到代码区,运行时不能修改这块内存。
未初始化数据区(BSS)
载入的是可执行文件的BSS段,可以与数据段分开也可以紧靠。 数据段存储的数据(全局未初始化、静态未初始化数据)的生命周期就是整个程序运行过程。
全局初始化数据区/静态数据区(data)
加载的是可执行文件的数据段,数据段存储的数据(全局初始化、静态初始化数据、字面常量(只读))的生命周期就是整个程序运行过程。
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配和释放,存放函数参数值、返回值、局部变量等,在运行过程中实时加载和释放该程序。 因此,局部变量的生命周期是从申请到释放栈空间。
堆区(heap)
堆是一个很大的容器,它的容量比栈大很多,但是它不像栈有先进后出的顺序。 用于动态内存分配。 堆位于内存中BSS区和栈区之间。 一般由程序员分配和释放,如果程序员不释放,会在程序结束时被操作系统收回。
2.3.2 分区模型
2.3.2.1 栈区
内存管理由系统执行。 主要存放函数参数和局部变量。 函数执行完成后,系统自行释放栈区内存,无需用户管理。
#char* func(){
char p[] = "hello world!"; //在栈区存储 乱码
printf("%s\n", p);
return p;
}
void test(){
char* p = NULL;
p = func();
printf("%s\n",p);
}
2.3.2.2 堆区
由程序员手动申请,手动释放。 如果没有手动释放,程序结束后会被系统回收。 生命周期是程序的整个运行周期。 使用或new申请堆。
char* func(){
char* str = malloc(100);
strcpy(str, "hello world!");
printf("%s\n",str);
return str;
}
void test01(){
char* p = NULL;
p = func();
printf("%s\n",p);
}
void allocateSpace(char* p){
p = malloc(100);
strcpy(p, "hello world!");
printf("%s\n", p);
}
void test02(){
char* p = NULL;
allocateSpace(p);
printf("%s\n", p);
}
堆分配内存API:
#include
void *calloc(size_t nmemb, size_t size);
功能:
在内存动态存储区分配一块nmemb块长度大小字节的连续区域。 自动将分配的内存设置为 0。
范围:
nmemb:需要的内存单元个数 size:每个内存单元的大小(单位:byte)
返回值:
成功:分配空间的起始地址
失败:空
#include
void *realloc(void *ptr, size_t size);
功能:
堆中重新分配或函数分配的内存空间大小。 增加的内存不会自动清理,需要手动清理。 如果指定地址后面有连续空间,则在已有地址的基础上增加内存。 如果指定地址后没有空间,则重新分配新的连续内存,将旧内存的值复制到新内存中,同时释放旧内存。
范围:
ptr:之前使用或分配的内存地址,如果此参数等于NULL,则功能与
size:重新分配内存的大小,单位:byte
返回值:
成功:新分配的堆内存地址
失败:空
void test01(){
int* p1 = calloc(10,sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i ++){
p1[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ",p1[i]);
}
printf("\n");
free(p1);
}
void test02(){
int* p1 = calloc(10, sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i++){
p1[i] = i + 1;
}
int* p2 = realloc(p1, 15 * sizeof(int));
if (p2 == NULL){
return;
}
printf("%d\n", p1);
printf("%d\n", p2);
//打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
//重新赋值
for (int i = 0; i < 15; i++){
p2[i] = i + 1;
}
//再次打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
free(p2);
}
2.3.2.3 全局/静态区域
全局静态区中的变量在编译时已经分配了内存空间并进行了初始化。 这块内存在程序运行过程中一直存在,主要存放全局变量、静态变量和常量。
注意:
(1) 这里不区分已初始化和未初始化的数据区,因为如果静态存储区的变量不显示初始化,编译器会自动按照默认的方式进行初始化,即在静态存储区中没有未初始化的变量静态存储区。
(2) 全局静态存储区中的常量分为常量变量和字符串常量。 一旦初始化,它们就不能被修改。 静态存储中的常量变量是全局变量,不同于局部常量变量。 不同的是,局部常量变量存储在栈中,可以通过指针或引用间接修改,而存储在静态常量区的全局常量变量不能被间接修改。
(3) 字符串常量存放在全局/静态存储区的常量区。
int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL
//那么全局static int 和 全局int变量有什么区别?
void test(){
static int v4 = 20; //全局/静态区
}
char* func(){
static char arr[] = "hello world!"; //在静态区存储 可读可写
arr[2] = 'c';
char* p = "hello world!"; //全局/静态区-字符串常量区
//p[2] = 'c'; //只读,不可修改
printf("%d\n",arr);
printf("%d\n",p);
printf("%s\n", arr);
return arr;
}
void test(){
char* p = func();
printf("%s\n",p);
}
2.3.2.4 总结
在理解C/C++内存分区时,经常会遇到以下名词:数据区、堆、栈、静态区、常量区、全局区、字符串常量区、文本常量区、代码区等,初学者一头雾水。 在此,尝试理清以上分区之间的关系。
数据区包括:堆、栈、全局/静态存储区。
可以说,实际上C/C++的内存分区只有两个,分别是代码区和数据区。
2.3.3 函数调用模型
2.3.3.1 函数调用流程
堆栈(stack)是现代计算机程序中最重要的概念之一。 几乎每个程序都使用堆栈。 没有栈,就没有我们今天能看到的函数,没有局部变量,更没有计算机语言 。 在解释为什么栈如此重要之前,我们先了解一下传统栈的定义:
在经典计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(push,push),也可以将压入栈中的数据弹出(pop,pop),但栈容器必须跟一个规则:先入栈的数据出栈(First In Last Out,FILO)。
在经典操作系统中,堆栈总是向下增长。 压栈操作使栈顶地址减少,出栈操作使栈顶地址增加。
栈在程序的运行中起着极其重要的作用。 最重要的是,栈保存了一次函数调用需要维护的信息,通常称为栈帧(Stack frame)或活动记录( )。 一个函数调用过程需要的信息一般包括以下几个方面:
从下面的代码中,我们分析以下函数的调用过程:
int func(int a,int b){
int t_a = a;
int t_b = b;
return t_a + t_b;
}
int main(){
int ret = 0;
ret = func(10, 20);
return EXIT_SUCCESS;
}
2.3.3.2 调用约定
现在,我们对函数调用的过程有了一个大概的了解。 这期间有一个现象,就是函数的调用者和被调用者对函数调用的理解是一致的。 比如,他们都同意函数的参数是按照某个固定的方法压栈的。 如果不这样做,该功能将无法正常工作。
如果函数调用者在传递参数时先压入a参数,再压入b参数,而被调用函数认为先压入b,后压入a,则被调用函数在使用a、b值时,它会被逆转。
因此,函数的调用者和被调用者必须对如何调用函数有明确的约定。 只有双方遵循相同的约定,才能正确调用函数。 这样的约定称为“调用约定()”。 一个调用约定一般包括以下几个方面:
传递函数参数的顺序和方法
传递函数的方式有很多种,最常见的是通过栈传递。 函数的调用者将参数压入栈中,函数自己从栈中取出参数。 对于具有多个参数的函数,调用约定指定函数调用者将参数压入堆栈的顺序:从左到右,或从右到左。 一些调用约定还允许使用寄存器传递参数以提高性能。
堆栈是如何维护的
函数将参数压入栈后,调用函数体,然后需要将压入栈的参数全部出栈,使函数调用前后栈保持一致。 这种弹出工作可以由函数的调用者来完成,也可以由函数本身来完成。
为了在链接时区分调用约定,调用约定必须修改函数本身的名称。 不同的调用约定有不同的名称修改策略。
其实在C语言中,有多种调用约定,默认的是cdecl。 任何未指定调用约定的函数默认为 cdecl 约定。 比如上面func函数的声明,它完整的写法应该是:
int _cdecl func(int a,int b);
注意:cdecl 不是标准的关键字,不同的编译器可能写法不一样。 比如gcc中就没有这个关键字,而是用((cdecl))。
2.3.3.2 函数变量传递分析
2.3.4 栈的增长方向和内存存储方向
//1. 栈的生长方向
void test01(){
int a = 10;
int b = 20;
int c = 30;
int d = 40;
printf("a = %d\n", &a);
printf("b = %d\n", &b);
printf("c = %d\n", &c);
printf("d = %d\n", &d);
//a的地址大于b的地址,故而生长方向向下
}
//2. 内存生长方向(小端模式)
void test02(){
//高位字节 -> 地位字节
int num = 0xaabbccdd;
unsigned char* p = #
//从首地址开始的第一个字节
printf("%x\n",*p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
}