数据类型
编译器制定出的数据类型,为了更好的管理内存
C语言标准
ANSI 美国国家标准协会指定出的标准,在89年指定出第一套标准 C89标准
typedef使用
- 主要用途:给类型起别名
- 可以简化struct 关键字
- 可以区分数据类型
- 提高代码移植性
void使用
- void无类型,不能用void创建变量
- void用途,限定返回值类型,函数参数
- 限定函数中的参数列表
- void* 万能指针,可以不需要强制类型转换 转给其他指针赋值
sizeof操作符
// sizeof不是一个函数,是一个运算符
// sizeof的返回值是一个unsigned int
unsigned int a = 10;
// 如果一个unsigned int和int运算,结果会统一转换为unsigned int
if (sizeof(a) - 20 >0) {
printf("大于0\n");
} else {
printf("小于0\n");
}
// 当数组名传入到函数中,会退化成一个指针,指针指向数组中第一个元素的地址
void calculateArray(int array[])
{
printf("size = %d\n", sizeof(array));
}
void test03()
{
// 'sizeof' on array function parameter 'array' will return size of 'int*'
int array[] = {0, 1, 2, 3, 4, 5, 6, 7};
// 这里统计的是整个数组的长度
printf("sizeof = %d\n", sizeof(array));
calculateArray(array);
}
变量的修改方式
直接修改
间接修改(指针)
- 通过指针对内存进行修改
- 对自定义数据类型进行了修改
//对于自定义数据类型
struct Person
{
char a; // 0 ~ 3
int b; // 4 ~ 7
char c; // 8 ~ 11
int d; // 12 ~ 15
};
void test02()
{
struct Person p1 = { 'a', 10, 'b', 20 };
//直接修改 d 属性
p1.d = 1000;
//间接修改 d 属性
struct Person * p = &p1;
p->d = 2000;
// p+1跳过一个struct Person,跳过16个地址
printf("%d\n", p);
printf("%d\n", p+1);
char * pPerson = p;
// 指针的类型不同,+1跳过的步长不同
printf("d = %d\n", *(int*)(pPerson + 12));
//
printf("d = %d\n", *(int*)((int*)pPerson +3) );
}
内存分区
运行前
全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和t)和常量数据(如字符串常量)。
未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
代码区
共享的
只读的
数据区
已初始化的全局变量、静态变量、常量
未初始化的全局变量、静态变量、常量
运行后
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间
栈区注意事项 ,不要返回局部变量的地址
//栈 注意事项 ,不要返回局部变量的地址,局部变量在函数体执行完毕后会被释放,再次操作后就是非法操作,结果未知
int * func()
{
int a = 10;
return &a;
}
void test01()
{
int * p = func();
//结果已经不重要了,因为a的内存已经被释放了,我们没有权限去操作这块内存
printf("a = %d\n", *p);
printf("a = %d\n", *p);
}
char * getString()
{
char str[] = "hello world";
// 这里还是返回的栈区变量的地址
return str;
}
void test02()
{
char * p = NULL;
p = getString();
printf("%s\n", p);
}
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
- 在堆区开辟的数据,记得手动开辟,手动释放
- 如果在主调函数中没有给指针分配内存,那么被调函数中需要利用高级指针给主调函数中指针分配内存
//注意事项
//如果主调函数中没有给指针分配内存,被调函数用同级指针是修饰不到主调函数中的指针的
void allocateSpace( char * pp )
{
char * temp = malloc(100);
if (temp == NULL)
{
return;
}
memset(temp, 0, 100);
strcpy(temp, "hello world");
pp = temp;
}
void test02()
{
char * p = NULL;
allocateSpace(p);
printf("%s\n", p);
}
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);
free(p);
p = NULL;
}
数据区
放入是静态变量、全局变量、常量
static 和 extern 区别
- static 静态变量:编译阶段分配内存,只能在当前文件内使用,只初始化一次
- extern 全局变量,C语言下默认的全局变量前都隐藏的加了该关键字
//1、静态变量
static int a = 10; //特点:只初始化一次,在编译阶段就分配内存,属于内部链接属性,只能在当前文件中使用
void test01()
{
static int b = 20; //局部静态变量,作用域只能在当前test01中
//a 和 b的生命周期是一样的
}
//2、全局变量
extern int g_a = 100; //在C语言下 全局变量前都隐藏加了关键字 extern,属于外部链接属性
void test02()
{
//告诉编译器 g_b是外部链接属性变量,下面在使用这个变量时候不要报错
extern int g_b;
printf("g_b = %d\n", g_b);
}
const修饰的变量
- 全局变量
- 直接修改 失败
- 间接修改 失败 原因放在常量区,受到保护
- 局部变量
- 直接修改 失败
- 间接修改 成功 原因放在栈上
- 伪常量 不可以初始化数组
//1、const修饰的全局变量,即使语法通过,但是运行时候受到常量区的保护,运行失败
const int a = 10; //放在常量区
void test01()
{
//a = 100; //直接修改 失败
int * p = &a;
*p = 100;
printf("%d\n", a);
}
//2、const修饰的局部变量
void test02()
{
const int b = 10; //分配到栈上
//b = 100; //直接修改失败的
//间接修改 成功, 原因是分配到栈上的,可以被修改
//C语言下 称为伪常量
int * p = &b;
*p = 100;
printf("b = %d\n", b);
//int a[b];//伪常量是不可以初始化数组的
}
字符串常量
- 不同的编译器可能有不同的处理方式
- 有些编译器可以修改字符串常量,有些不可以
- 有些编译器将相同的字符串常量看成同一个
函数调用流程
宏函数
在一定程度上会比普通函数效率高,普通函数会有入栈和出栈的时间开销
- 将比较频繁短小的函数 写为宏函数,直接跑源码
- 优点: 以空间换时间
调用惯例
- 主调函数和被调函数都必须有一致的约定,才可以正确的调用函数,这个约定我们称为调用惯例
- 调用惯例包含的内容: 出栈方、参数的传入顺序、函数名称的修饰
- c和c++下默认的调用惯例为 cdecl
出栈方:主调函数
参数传递顺序:从右往左
名称修饰:下划线+函数名
变量传递分析
void B()
{
}
void A()
{
// 在函数A、B中可以使用、在main中使用不了
int a;
B();
}
int main()
{
// 在main函数和A、B中都可以使用
int a;
A();
}
栈的生长方向
// 栈的生长方向
void test01() {
int a = 0; // 栈低, 高地址
int b = 0;
int c = 0;
int d = 0; // 栈顶, 地地址
printf("%d %d %d %d\n", &a, &b, &c, &d);
}
int main(void) {
test01();
return 0;
}
内存存储方式
小端对齐:
高位字节数据–高地址
低位字节数据–低地址
大端对齐:
高位字节数据–小地址
低位字节数据–高地址
大小端验证方式:
// 内存存储方式
void test02() {
// 11是高位 44是地位
int a = 0x11223344;
char *p = (char *) &a;
// 小端:高位字节,高地址
printf("%x\n", *p); // 44 低位字节 低地址
printf("%x\n", *(p+1));
printf("%x\n", *(p+2));
printf("%x\n", *(p+3)); // 11 高位字节 高地址,因为p增加3,肯定比p的地址要大
}