也可通过 notion 或者 我的博客 访问
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 |
局部变量:作用域及生命期为当前函数;
静态局部变量:作用域为当前函数,生命期为整个源程序。
全局变量:作用域及生命期为整个源程序。
静态局部变量:作用域为当前文件,生命期为整个源程序。
内存由一个个内存单元组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和内存单元格式唯一对应且永久绑定。
在程序运行时,CPU实际上只认识内存地址,不关心这个地址所代表的的内存空间在哪里,如何分布的这些问题,因为硬件设计保证了按照这个地址就一定能找到这个内存空间,所以内存单元的两个概念:地址和空间是两个方面的问题。
内存编址是以字节(8bit)为单位。
数据类型是用来定义变量的,而变量需要存储、运行在内存中的,所以数据类型和内存相匹配才能获得最好的性能。
例如:在32位系统中定义变量最好int,因为这样效率最高,原因就是32位系统本身配合内存也是32位,虽然也能定义8位的 char,或者是16位的short类型,但实际访问效率没有int高,但如果都用int,会导致内存浪费,所以实际开发中,需要考虑需要的是省内存还是运行效率。
内存对齐不是逻辑上的问题,是硬件的问题。
对齐访问配合硬件,所以效率很高,非对齐访问因为和硬件本身不搭配,所以效率不高,但由于兼容性的问题,一般硬件也都提供非对齐访问,但效率很低
程序执行前
一个程序本质上都是由 bss段、data段、text段三个段组成。[1、5]
C/C++程序经编译器编译后产生的可执行文件,其大小由text段和data段决定。[3、5]
原因:从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间。[3]
程序执行时
程序在执行时,会产生临时变量或者函数返回值,还会有函数中的动态分配地址空间(如 malloc、new),此时才会出现堆(heap)和栈(stack)[4]。
栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,用来函数切换时保存现场。其操作方式类似于数据结构中的栈。栈地址是向下增长。
满增栈 满减栈 空增栈 空减栈
堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。堆地址一般是向上增长。
引用:
[1] 浅谈text段、data段和bss段
[2] 终于知道什么叫BSS段
[3] 基础知识——嵌入式内存使用分析(text data bss及堆栈)
[4] 程序各个段text,data,bss,stack,heap
[5] (深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack)
程序执行时,内存也可按如下分区:
动态存储区
静态存储区(全局区 static):静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在。存放:字符串常量、全局常量(const 变量)、静态变量、全局变量等;
可分全局已初始化区和全局未初始化区(即上文BSS段中的变量),这里未做区分。静态存储区内的变量若未初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。
程序代码区:存放程序编译后的二进制代码,不可寻址区。
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"优化成一个地方
}
引用:
C/C++的四大内存分区和常量的存储位置
内存分区
大端(存储)模式: 是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中;
小端(存储)模式: 是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。
因为在计算机系统中,我们以字节为存储单元,每个地址单元都对应着一个字节,一个字节为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");
}
引用:
大端 / 小端,三种判断方法
C++怎么判断大小端模式
对内向上对齐,整体4字节对齐
结构体(struct)的数据成员,第一个数据成员存放的地址为结构体变量偏移量为0的地址处。
其他结构体成员自身对齐时,存放的地址为min{有效对齐值为自身对齐值, 指定对齐值} 的最小整数倍的地址处。
自身对齐值:结构体变量里每个成员的自身大小 ;
指定对齐值:有宏 #pragma pack(N) 指定的值,这里面的 N一定是2的幂次方。如1,2,4,8,16等。如果没有通过宏那么在32位Linux主机上默认指定对齐值为4,64位的默认对齐值为8,AMR CPU默认指定对齐值为8;
有效对齐值:结构体成员自身对齐时有效对齐值为自身对齐值与指定对齐值中较小的一个。
总体对齐时,字节大小是min{所有成员中自身对齐值最大的,指定对齐值} 的整数倍。
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n), n = 1,2,4,8,16
来改变这一系数,其中的n就是你要指定的“对齐系数”。
32位系统默认4字节对齐,64位系统默认8字节对齐。
//此代码在64位Linux下编写
typedef struct _st_struct2
{
char a;
int c;
short b;
}st_struct2;
printf("%ld\n",sizeof(st_struct2));
打印结果为:12
min{4, 8}=4
的整数倍。此时内存中共占10个字节,又要求是4的整数倍,所以sizeof(st_struct1)=12
。该程序未指定对齐系数,因此为系统默认对齐系数。
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个字节,冒号后面的数字为指定这个变量占几位。
const 修饰变量:该变量为常量,不可修改,代表 只读。必须要给变量初始化。
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 指针指向空间的内容。
// 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 Person p; //在对象前加入const,变为常对象
//p.m_A = 100; // 常对象不能修改成员变量的值,但是可以访问
p.m_B = 100; // 但是常对象可以修改mutable修饰成员变量
//常对象只能调用常函数
p.showPerson();
//p.func(); //常对象 不可以调用普通成员函数,因为普通成员函数可以修改属性
提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
在编译阶段起作用。
详细解析
在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。
内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,而主函数在调用一般函数时,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码。
详细解析
预处理 编译 汇编 链接
strcpy函数的缺陷:可能会内存溢出,strncpy 不会,最安全的是 strncpy_s。
函数的返回值为指针,其声明的形式如下:
ret * func(args, ...);
其中,func
是一个函数,args
是形参列表,ret *
作为一个整体,是 func
函数的返回值,是一个指针的形式。
下面举一个具体的实例来做说明:
# include
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),程序执行完后不会被释放。
储存函数入口地址的指针。声明形式如下:
ret (*p)(args, ...);
其中,ret
为返回值,*p
作为一个整体,代表的是指向该函数的指针,args
为形参列表。其中p
被称为函数指针变量 。
函数指针所占用字节与CPU本身的数据位宽一致,如:32位机器为32bit,而64位机器为64bit。
#include
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;
}
为基本数据类型定义新的类型名
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;
详见:typedef的用法,C语言typedef详解
首先,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;
char* arr[4]
:指针数组可以说成是”指针组成的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型。char (*pa)[4]
:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。详见:指针数组与数组指针详解
一维数组的10种访问方式:
#include
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++
会报错。
二维数组:
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]
.
”(类成员访问运算符).*
"(类成员指针访问运算符)::
”(域运算符)siezof
”(长度运算符)?:
”(条件运算符)%
取余运算符