1. 字,字节,位的关系
- 位(bit) 来自英文bit,音译为“比特”,表示二进制位。位是计算机内部数据储存的最小单位。
- 字节(byte) 字节来自英文Byte,音译为“拜特”,习惯上用大写的“B”表示。 字节是计算机中数据处理的基本单位,内存编址以字节为单位。
- 字 (word)计算机进行数据处理时,一次存取、加工和传送的数据长度称为字。一个字通常由一个或多个(一般是字节的整数位)字节构成。
1.1 相互转换
- 1字节(byte) = 8位(bit)
- 在16位的系统中(比如8086微机) 1字(word)= 2字节(byte)= 16(bit) 在32位的系统中(比如win32) 1字(word)= 4字节(byte)= 32(bit) 在64位的系统中(比如win64) 1字(word)= 8字节(byte)= 64(bit)
1.2 常用的变量类型所占字节
C语言中的基本数据类型有: char、short、int、long、float、double。
char:1个字节、8位;
short:2个字节、16位;
int:8/16位通常是2字节、16位;GCC编译器下32/64位的CPU为4字节、32位;
char*:指针类型,所有指针类型均与CPU本身的数据位宽一致,如:32位机器为4字节、32bit,而64位机器为8字节、64bit。
整型这个整,就体现在它和CPU本身的数据位宽是一样的,例如32位的CPU,int 就是32位。
不同变量在不同位数的处理器下所占的字节数
变量类型 | 8/16位处理器 | 32位处理器 | 64位处理器 |
---|---|---|---|
char | 1 | 1 | 1 |
short int | - | 2 | 2 |
int | 2 | 4 | 4 |
long int | 4 | 4 | 8 |
long long int | - | 8 | 8 |
char* | 1/2 | 4 | 8 |
float | 4 | 4 | 4 |
double | 8 | 8 | 8 |
2. 变量的作用域及生命周期
局部变量:作用域及生命期为当前函数;
静态局部变量:作用域为当前函数,生命期为整个源程序。
全局变量:作用域及生命期为整个源程序。
静态局部变量:作用域为当前文件,生命期为整个源程序。
3. 内存相关
3.1 内存编址:
内存由一个个内存单元组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和内存单元格式唯一对应且永久绑定。 在程序运行时,CPU实际上只认识内存地址,不关心这个地址所代表的的内存空间在哪里,如何分布的这些问题,因为硬件设计保证了按照这个地址就一定能找到这个内存空间,所以内存单元的两个概念:地址和空间是两个方面的问题。
内存编址是以字节(8bit)为单位。
3.2 内存和数据类型:
数据类型是用来定义变量的,而变量需要存储、运行在内存中的,所以数据类型和内存相匹配才能获得最好的性能。 例如:在32位系统中定义变量最好int,因为这样效率最高,原因就是32位系统本身配合内存也是32位,虽然也能定义8位的 char,或者是16位的short类型,但实际访问效率没有int高,但如果都用int,会导致内存浪费,所以实际开发中,需要考虑需要的是省内存还是运行效率。
3.3 内存对齐:
内存对齐不是逻辑上的问题,是硬件的问题。 对齐访问配合硬件,所以效率很高,非对齐访问因为和硬件本身不搭配,所以效率不高,但由于兼容性的问题,一般硬件也都提供非对齐访问,但效率很低
3.4 内存分区
- 程序本质上由 .bss段、数据段(.data+.rodata)、.text段三个段组成(执行前)。[1、5]
- .text段:代码段。存放代码和字符串,编译时确定,不可寻址区,只读。
- .rodata段:常量区。存放字符串常量和全局const 变量。存放在编译阶段(而非运行时)就能确定的数据,只读状态,不可修改。
- .data段:已初始化全局/静态区。已初始化的(非 0)全局变量 、全局或局部静态变量,存放在编译阶段(而非运行时)就能确定的数据,可读可写。
- .bss段:未初始化全局/静态区。存未初始化的静态变量 、 未初始化的全局变量 和 初始化为0的全局变量。BSS是Unix链接器产生的未初始化数据段,不包含任何数据,只是简单的维护开始和结束的地址[2]。一般在初始化时bss段变量将会清零(bss段属于静态内存分配,即程序一开始就将其清零了),可读可写。
C/C++程序经编译器编译后产生的可执行文件,其大小由text段和data段决定。[3、5]
原因:从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间。[3]
- 程序执行时,会产生临时变量或者函数返回值,还会有函数中的动态分配地址空间(如 malloc、new),此时才会出现堆(heap)和栈(stack)[4]。
- 堆区(heap): 由程序猿手动申请,手动释放,程序结束时可能由OS回收。使用malloc或者new进行堆的申请。注意它与数据结构中的堆是两回事,分配方式类似于链表。堆地址一般是向上增长。
-
栈区(stack):由编译器自动分配释放 ,存放函数的参数、局部变量、局部常量(const 变量),用来函数切换时保存现场。其操作方式类似于数据结构中的栈。栈地址是向下增长。
满增栈 满减栈 空增栈 空减栈
参照[2]绘制
int a = 0;//静态全局变量区 全局初始化区
char *p1; //静态全局变量区 中的 全局未初始化区,编译器默认初始化为 NULL
void main()
{
int b; //栈
char s[] = "abc";//栈
char *p2 = "123456";//p2在栈上,123456在字符串常量区
static int c = 0; //c在静态变量区,0为文字常量,在代码区
const int d = 0; //栈
static const int d;//静态常量区
p1 = (char *)malloc(10);//分配得来得10字节在堆区。
strcpy(p1, "123456"); //123456放在字符串常量区,编译器可能会将它与p2所指向的"123456"优化成一个地方
}
引用: [1] 浅谈text段、data段和bss段 [2] 终于知道什么叫BSS段 [3] 基础知识——嵌入式内存使用分析(text data bss及堆栈) [4] 程序各个段text,data,bss,stack,heap [5] (深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack) [6] C/C++的四大内存分区和常量的存储位置 [7] 内存分区
3.5 C/C++ 内存分区
程序执行时,内存也可按如下分区:
- 动态存储区
- 堆区(heap):由程序猿手动申请,手动释放。使用malloc或者new进行堆的申请。
- 栈区(stack):由编译器自动分配释放。存放函数的参数、局部变量、局部常量(const 变量);
- 静态存储区(全局区 static):静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在。存放:字符串常量、全局常量(const 变量)、静态变量、全局变量等;
可分全局已初始化区和全局未初始化区(即上文BSS段中的变量),这里未做区分。静态存储区内的变量若未初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。
- 常量存储区(const):常量占用内存,只读状态,不可修改。存放字符串常量和全局常量。
- 程序代码区:存放程序编译后的二进制代码,不可寻址区。
int a = 0;//静态全局变量区 全局初始化区
char *p1; //静态全局变量区 中的 全局未初始化区,编译器默认初始化为 NULL
void main()
{
int b; //栈
char s[] = "abc";//栈
char *p2 = "123456";//p2在栈上,123456在字符串常量区
static int c = 0; //c在静态变量区,0为文字常量,在代码区
const int d = 0; //栈
static const int d;//静态常量区
p1 = (char *)malloc(10);//分配得来得10字节在堆区。
strcpy(p1, "123456"); //123456放在字符串常量区,编译器可能会将它与p2所指向的"123456"优化成一个地方
}
4. 处理器大小端
大端(存储)模式: 是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中; 小端(存储)模式: 是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。
因为在计算机系统中,我们以字节为存储单元,每个地址单元都对应着一个字节,一个字节为8bit。而在C语言中,不仅仅是一个字节来存储一个数据,除了一个字节的char,还有两个字节的short,四个字节的int等等(看具体编译器)。另外,对于位数大于8位的处理器,例如32位的处理器,由于寄存器的宽度大于一个字节,那么就有如何将多个字节进行排布的问题,于是就出现了大小端的问题。下面举个栗子:
判断方法:
-
通过联合体判断
定义联合体,一个成员是多字节,一个是单字节,给多字节的成员赋一个最低一个字节不为0,其他字节为0 的值,再用第二个成员来判断,如果第二个字节不为0,就是小端,若为0,就是大端。
void judge_bigend_littleend2() { union { int i; char c; }un; un.i = 1; if (un.c == 1) printf("小端\n"); else printf("大端\n"); }
-
通过强制类型转换判断
将int 48存起来,然后取得其地址,再将这个地址转为char* 这时候,如果是小端存储,那么char*指针就指向48;48对应的ASCII码为字符‘0’;
void judge_bigend_littleend3() { int i = 48; int* p = &i; char c = 0; c = *((char*)p); if (c == '0') printf("小端\n"); else printf("大端\n"); }
5. 结构体
5.1 结构体的对齐
对内向上对齐,整体4字节对齐
-
结构体(struct)的数据成员,第一个数据成员存放的地址为结构体变量偏移量为0的地址处。
-
其他结构体成员自身对齐时,存放的地址为min{有效对齐值为自身对齐值, 指定对齐值} 的最小整数倍的地址处。
自身对齐值:结构体变量里每个成员的自身大小 ;
指定对齐值:有宏 #pragma pack(N) 指定的值,这里面的 N一定是2的幂次方。如1,2,4,8,16等。如果没有通过宏那么在32位Linux主机上默认指定对齐值为4,64位的默认对齐值为8,AMR CPU默认指定对齐值为8;
有效对齐值:结构体成员自身对齐时有效对齐值为自身对齐值与指定对齐值中较小的一个。 -
总体对齐时,字节大小是min{所有成员中自身对齐值最大的,指定对齐值} 的整数倍。
5.2 对齐系数
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n), n = 1,2,4,8,16
来改变这一系数,其中的n就是你要指定的“对齐系数”。
32位系统默认4字节对齐,64位系统默认8字节对齐。
5.3 结构体大小计算
//此代码在64位Linux下编写
typedef struct _st_struct2
{
char a;
int c;
short b;
}st_struct2;
printf("%ld\n",sizeof(st_struct2));
打印结果为:12
- a是char类型,占1个字节。第一个数据成员,放在结构体变量偏移量为0 的地址处。
- c是int类型,占4个字节,我们根据结构体对齐规则可知,c的有效对齐值为4。对齐到4的整数倍地址,即地址偏移量为4处。在内存中存放的位置为4、5、6、7。
- b是short类型,占2个字节,我们根据结构体对齐规则可知,b的有效对齐值为2。对齐到2的整数倍地址,即地址偏移量为8处。在内存中存放的位置为8、9。
- 结构体总对齐字节大小为
min{4, 8}=4
的整数倍。此时内存中共占10个字节,又要求是4的整数倍,所以sizeof(st_struct1)=12
。
5.4 结构体的位域
该程序未指定对齐系数,因此为系统默认对齐系数。
struct ftl_block_status
{
zx_uint32_t erase_times : 28;
zx_uint32_t block_status: 3;
zx_uint32_t reserv : 1;
struct ftl_pagestatus page_status; /*status bitmap for each page inside of block */
};
结构体里的这三个变量总体只占4个字节,冒号后面的数字为指定这个变量占几位。
6. static修饰符
- static修饰局部变量:将局部变量转换成静态局部变量,在全局数据区分配内存空间,编译器自动对其初始化,即使函数返回,它的值也会保持不变。其作用域为局部作用域。
- static修饰全局变量:将其转换成静态全局变量,仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。
- static修饰函数:将其转换成静态函数,只能在声明它的文件中可见,其他文件不能引用该函数。不同的文件可以使用相同名字的静态函数,互不影响。
- static修饰成员变量:将其转换成静态成员变量。
- 在编译阶段分配内存。静态成员变量存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中初始化。
- 所有对象共享同一份数据。静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。
- 由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作。
- 和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则。
- 类内声明,类外初始化。静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>值>静态数据成员名>类名>数据类型>
- 类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>静态数据成员名>类类型名>静态数据成员名>类对象名>
- static修饰成员函数:将其转换成静态成员函数。
- 所有对象共享同一个函数。静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
- 出现在类体外的函数定义不能指定关键字static。
- 静态成员函数只能访问静态成员变量。非静态成员函数可以任意地访问静态成员函数和静态数据成员。
7. const 修饰
-
const 修饰变量:该变量为不可改变的变量,而非常量,代表 只读。必须要给变量初始化。
常量存在flash中,而const修饰的变量不一定在flash中,具体看编译器。如CodeWarrior将const变量存在flash中。
const char LEN = 10 char array[LEN]; // 编译报错,因为编译阶段LEN无效, // 而采用宏定义 #defin LEN 10的话则有效。
-
const 修饰指针:
-
const修饰指针:常量指针。指针指向可以改,指针指向的值不可以更改,但是还是可以通过其他的引用来改变变量的值的。
int a = 5; const int* p1 = &a; a = 6;
-
const修饰常量:指针常量。指针指向不可以改,指针指向的值可以更改。
int* const p2 = &a;
区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。
-
const即修饰指针,又修饰常量。指针指向不可以改,指针指向的值也不可以更改。
const int* const p3 = &a;
-
-
const 修饰形参:
根据常量指针与指针常量,const修饰函数的参数也是分为三种情况。
-
防止修改指针指向的值
void StringCopy(char *strDestination, const char *strSource);
其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
-
防止修改指针指向的地址
void swap ( int * const p1 , int * const p2 )
指针p1和指针p2指向的地址都不能修改。
-
以上两种的结合
-
-
const 修饰函数的返回值:函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。 例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();
-
const 修饰类成员函数:
- 常函数:const 修饰的是 this 指针指向空间的内容。
- 成员函数后加const后我们称为这个函数为常函数。
- 常函数内不可以修改成员属性。
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改。
// this指针的本质 是指针常量 也就是指针的指向是不可以修改的 // this指针相当于Person * const this // 在成员函数后面加const,修饰的是this指向的内容,让指针指向的值也不可以修改,相当于const Person * const this void showPerson() const { this->m_B = 100; //m_A = 100; //相当于 this->m_A = 100; //this = NULL; //this指针不可以修改指针的指向的 } int m_A; mutable int m_B; //特殊变量,即使在常函数中,也可以修改这个值,加关键字 mutable
- 常对象:
- 声明对象前加const称该对象为常对象。
- 常对象只能调用常函数。
const Person p; //在对象前加入const,变为常对象 //p.m_A = 100; // 常对象不能修改成员变量的值,但是可以访问 p.m_B = 100; // 但是常对象可以修改mutable修饰成员变量 //常对象只能调用常函数 p.showPerson(); //p.func(); //常对象 不可以调用普通成员函数,因为普通成员函数可以修改属性
- 常函数:const 修饰的是 this 指针指向空间的内容。
8. volatile 修饰
提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
在编译阶段起作用。
9. inline 修饰函数:内联函数
在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。
内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,而主函数在调用一般函数时,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码。
10. 编译的四个步骤
预处理 编译 汇编 链接
11. 字符串拷贝函数
strcpy函数的缺陷:可能会内存溢出,strncpy 不会,最安全的是 strncpy_s。
12. 指针函数和函数指针
12.1 指针函数
函数的返回值为指针,其声明的形式如下:
ret * func(args, ...);
其中,func
是一个函数,args
是形参列表,ret *
作为一个整体,是 func
函数的返回值,是一个指针的形式。
下面举一个具体的实例来做说明:
# include <stdio.h>
int * func_sum(int n)
{
static int sum = n; // 必须加 static
int *p = ∑
return p;
}
int main(void)
{
int num = 0;
scanf("%d", &num);
int *p = func_sum(num);
printf("sum:%d\n", *p);
return 0;
}
⚠️ 指针函数返回局部变量地址时,必须使用 静态局部变量。因为局部变量储存在 栈区,函数执行完后,该局部变量地址会被释放,在执行后面程序时,该地址可能被其他变量占用,地址里的值就会被修改。而静态局部变量储存在全局区(static data),程序执行完后不会被释放。
12.2 函数指针
储存函数入口地址的指针。声明形式如下:
ret (*p)(args, ...);
其中,ret
为返回值,*p
作为一个整体,代表的是指向该函数的指针,args
为形参列表。其中p
被称为函数指针变量 。
函数指针所占用字节与CPU本身的数据位宽一致,如:32位机器为32bit,而64位机器为64bit。
定义及初始化:
#include <stdio.h>
int max(int a, int b)
{
return a > b ? a : b;
}
int callback(int a, int b, int (*p)(int, int))
{
return p(a, b);
}
int main()
{
int (*p)(int, int); //函数指针的定义
//int (*p)(); //函数指针的另一种定义方式,不过不建议使用
//int (*p)(int a, int b); //也可以使用这种方式定义函数指针
p = max; //函数指针初始化
int ret = p(10, 15); //函数指针的调用
//int ret = (*max)(10,15);
//int ret = (*p)(10,15);
//以上两种写法与第一种写法是等价的,不过建议使用第一种方式
//也可以作为函数的形参进行传递
int ret1 = callback(10, 15, max);
printf("max = %d \n", ret);
printf("max = %d \n", ret1);
return 0;
}
13. typedef关键字
-
为基本数据类型定义新的类型名
typedef unsigned int COUNT;
-
为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct tagPoint { double x; double y; double z; } Point;
-
为数组定义简洁的类型名称
typedef int INT_ARRAY_100[100]; INT_ARRAY_100 arr;
-
为指针定义简洁的名称
typedef char* PCHAR; PCHAR pa;
14. sizeof和strlen
首先,strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。大部分编译程序在编译的时候就把 sizeof 计算过了,而 strlen 的结果要在运行的时候才能计算出来。
sizeof
获得变量或数据类型的字节大小,可用于类、结构、共用体和其他用户自定义数据类型;strlen
返回的是该字符串的长度,遇到\0
结束,\0
本身不计算在内。
例:对于以下语句:
char *str1 = "asdfgh";
char str2[] = "asdfgh";
char str3[8] = {'a', 's', 'd'};
char str4[] = "as\0df";
执行结果是:
sizeof(str1) = 4; strlen(str1) = 6;
sizeof(str2) = 7; strlen(str2) = 6;
sizeof(str3) = 8; strlen(str3) = 3;
sizeof(str4) = 6; strlen(str4) = 2;
15. 数组
15.1 数组指针和指针数组
- 指针数组
char* arr[4]
:指针数组可以说成是”指针组成的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型。 - 数组指针
char (*pa)[4]
:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
详见:指针数组与数组指针详解
15.2 一维数组的访问方式
一维数组的10种访问方式:
#include<stdio.h>
int main()
{
int a[10]={0,1,2,3,4,5,6,7,8,9};
int *p=a;
printf("%d %d %d %d %d %d %d %d %d %d ",
0[a],*(p+1),*(a+2),a[3],p[4],5[p],(&a[5])[1],1[(&a[6])],(&a[9])[-1],9[&a[0]]);
return 0;
}
输出:0 1 2 3 4 5 6 7 8 9
数组的地址:
int main(){
int a[5]={1,2,3,4,5};
**int *ptr=(int*)(&a+1); //相当于int *ptr=*(&a+1); a指向int类型,&a指向数组类型
printf("%d,%d",*(a+1),*(ptr-1));
}
输出 2,5
💡 不可使用数组名自加,如
a++
会报错。
15.2 二维数组的访问方式
二维数组:
int arr[4][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}};
-
当成一维数组来访问二维数组
int *p = arr[0]; for(int i = 0 ;i<16;i++) cout << *(p + i) << ' ';
-
使用数组指针的方式访问二维数组
int (*p1)[4] = arr; //指向含有四个元素一维数组的首地址 for(int i = 0;i<4;i++) for(int j = 0;j<4;j++) cout << *(*(p1 + i)+j) << ' ';
-
使用指针数组的方式访问二维数组
int *p2[4]; //定义指针数组 for(int k = 0 ;k<4;k++) p2[k] = arr[k]; //每个指针指向行元素,存储每行首地址 for(int i = 0;i<4;i++) for(int j = 0;j<4;j++) cout << *(p2[i]+j) << ' '; //p2[i]已经存储并指向每行的首地址了
-
使用指针的指针&指针数组
int **pointer; //指向指针的指针 int *pp[4]; //指针数组 for(int i = 0; i < 4; i++) pp[i] = arr[i]; //每个指针指向行元素,存储每行首地址 pointer = pp; for(int i = 0; i < 4; i++) for(int j = 0; j < 4; j++) cout << *(*(pointer + i) + j) << ' ';
-
二维数组的第
i
行起始地址的表示方法arr+i *(arr+i) arr[i] &arr[i]
16. 运算符
16.1 不能重载的运算符
- ”
.
“(类成员访问运算符) - ”
.*
“(类成员指针访问运算符) - ”
::
“(域运算符) - “
siezof
“(长度运算符) - ”
?:
“(条件运算符)
16.2 运算对象必须是整型的运算符
%
取余运算符