一、指针、数组
1.指针的概念:是指指针所存在的内存单元存放的地址,这个地址指向的是
变量单元,看似指针指向变量。
2.指针的使用
指针初始化:1. 0值常量表达式,将const的0值赋给指针
(const int a =0; int *p=0; p=a;int *p=NULL;)
2.类型匹配的变量地址(int *p =&a;)
3.另一对象末的下一地址
4.同类型的另一个有效地址
3.指针与数组
(1)联系:
a.表达式中数组名就是指针
b.C语言中把数组下标作为指针的偏移量
c.作为函数的数组名等同于指针
(2)区别:
a.数组名在传参时退化为指针,指针不会退化。
(不能在函数内部给传参进来的指针sizeof)
b.数组是连续开辟的内存空间,指针不是,指针只用
一个地址空间(32位4个字节,64位8个字节)
c.数组可以通过下标随机访问,指针必须经过计算访问
d.数组不能进行++、--操作,指针可以
(3)指针数组、数组指针
指针数组是int *p[n],存有n个指针的数组
数组指针是int (*p)[n],指向n个int型数组的指针
区别:指针和数组的占用内存不同,
指针数组是数组,占用n个相应类型大小的空间;
数组指针是指针,占用一个指针的内存大小。
(4)指针与引用(只有在c++中才有引用)
引用总是指向一个对象,没有空引用(null reference),指针可以变化指向不同对象。(c++要求引用总是指向一个对象,所以必须有初值)
operator[ ]是必须返回的操作符,需要给其赋值,所以要有引用
区别:
1) 引用只是别名,指针是个实体
2) 引用使用不需要解引用,指针需要解引用
3) 引用定义时初始化,之后不可变化,指针可以变化
4) 引用没有const,指针有const,const指针不可变
5) 引用不能为空,指针可以
6) sizeof引用得到的是变量的大小,指针是本身存储的地址的大小(4个字节)
7) 引用和指针的++不一样
int *p="abcd";
sizeof(p)是4个字节
int a[ ]="abcd";
sizeof(a)是5个字节,有\0占一个字节
数组名看做是指针,取数组名地址++相当于移动整个数组大小
数组名不能是左值
结果
数组名取地址是代表整个数组
指针数组的初始化:
1. 利用循环初始化
2. 指针数组定义的时候直接初始化
指针数组在初始化的时候可以看做是二维数组实际上就是二维数组;
不能直接将数组名赋给指针数组,两者类型不同。
数组指针的初始化:
1. int (*p)[2]=NULL;
p=arr;
2. int (*p)[2]=NULL;
p=&arr[0];
二、宏
1.宏是什么?
语法替换,根据预定义的规则在预处理阶段进行宏展开
2.宏的优缺点
优点:执行速度快,在预处理阶段完成替换
缺点:
i. 多处调用宏,会造成代码膨胀
ii. 宏的优先级要注意
iii. 宏与类型无关,不进行类型检查
iv. 带有副作用的宏参数进行多次求值,可能得到意外的结果(++、--)
v. 由于宏在预处理阶段就进行替换,无法调试
3.宏和函数比较
► 代码长度
§ 宏替换后会使得代码的长度变长,增加代码体积
§ 函数调用的时候都会跳转至函数所在位置
► 执行速度
§ 宏因为在预处理阶段,执行速度快
§ 函数需要创建栈针,压参传参,返回值开销,速度较慢
► 操作符优先级
§ 宏需要注意优先级,直接替换,会出现意外的结果
§ 函数的结果可预估
► 参数类型
§ 无关参数类型,不进行类型检查
§ 函数与参数类型有关,参数类型不同,执行代码不同
4.宏和枚举的比较
a. 宏在预编译阶段替换,枚举在编译阶段确定值
b. 宏不能调试,枚举可以
c. 枚举一次可以定义多个相关值,宏只能定义一个
5.宏和内联函数的比较
a. 宏不能调试,内联函数可以
b. 宏对参数不进行类型检查,内联函数需要
c. 宏肯定被替换,内联是一种建议
d. 宏优先级会出问题,内联不会
6.typedef和define比较
a. typedef可以保证生命中的变量均为同一类型,define不能保证
b. define的类型可以扩展,typedef不可以扩展
三、static
> 修饰变量【static修饰的变量存在静态区,函数运行结束也不会销毁该静态变量的值】
○ 静态全局变量
§ 作用域是从定义处到文件末尾,无外链接属
○ 静态局部变量
§ 在函数体内部可以调用,函数外不可以调用
> 修饰函数
○ 使得函数失去外部链接属性,作用域只在本文件内部(避免函数名冲突)
初始化,未初始化的静态变量和全局变量程序会默认初始化为0,
未初始化的局部变量默认初始化为随机值
静态变量和全局变量的数据都存储在全局数据区。
函数内的一般变量存储栈区,函数结束就会销毁,而静态变量在结束时不会被销毁。
注:静态全局变量和全局变量的区别:
→ 全局变量有外部链接属性,作用域是整个工程;
→ 静态全局变量没有外部链接属性,作用域是本文件
→ 使用extern声明可以在别的文件中使用全局变量,静态全局变量不行
静态成员函数没有this指针
四、const
Ø 修饰变量
○ 只读属性,不是常量!!
○ 节省空间,提高效率,编译器通常不为const变量分配存储空间,而是直接保存在符号表,不需要多次访问保存
Ø 修饰函数
○ 修饰函数参数
§ 参数值在函数内部不可更改
○ 修饰函数返回值
§ 函数返回值不可更改
Ø 修饰指针(去掉类型名,const离谁近,谁就不可变)
○ const int *p等价于int const *p,p值可以更改,*p不能改
○ int* const p ,p值不能修改(p的指向),*p可以修改,也就是指向的内容可以更改但是指向的地址空间不可修改
○ const int* const p ,p和*p都不可修改,也就是p指向的空间的内容不可修改,p指向的地址空间也不可修改
const作为参数,以值传递的方式,减少参数的构造,拷贝,析构函数的重复使用。
非const可以转化为const
const不可以转化为非const
两个const成员函数的常量性不同可以重载
const关键字的优点:
1.在C++中可以复用代码,提高程序复用性。
2.相比于#define 可以指定类型,而#define不可以
五、函数传参
* 传参方式
○ 传值调用
§ 接受参数的函数获得参数的临时拷贝
○ 传址调用
§ 直接将参数地址传给调用函数,直接对原参数进行修改
* 数组传参
○ 一维数组传参
§ 当一维数组作为参数,编译器总是解析为指向其首元素的首地址的指针
○ 二维数组传参
§ 数组指针接收void fun(int(*arr)[3]);
§ 二维数组接收void fun(int arr[][3]);
○ 多维数组传参
§ 需要提供除最左边的其余维度的长度
* 指针传参
○ 修改指针变量需要二级指针来接收,实参传当前指针的地址
六、结构体内存对齐
► 内存对齐?
○ 编译器为每个“数据单元”安排合适的位置,C语言允许对内存对齐进行干预
► 为什么要进行内存对齐?
○ 合理的分配地址空间,提高效率,尽量减少访问内存次数
► 对齐规则
○ 数据成员对齐规则:结构体或联合的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack设定的数值和结构最大数据
○ 结构体作为成员,如果一个结构中有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍开始存储
► 为什么要对齐?
○ 平台原因(移植原因):某些硬件平台只能从某些特定的地址取特定的数据,不能任意访问数据,否则抛出异常。
○ 性能原因:栈应该尽可能在自然边界上对齐,为了访问未对齐的内存,处理器要做两次访问内存,对齐的内存只需要访问一次
► 计算内存对齐
○ 结构体struct
§ 结构体必须按照顺序来计算占用字节数
§ 最后的整体字节数是根据min(max(结构体类型的字节数),x)计算得到的结果
§ 必须是结果的倍数
○ 联合体union
§ 计算union中最长的占用字节数
§ 需要时最长字节类型的倍数
○ 嵌套类型
空结构体大小为1 ,进行占位,gcc默认对齐数为4,vs默认对齐数为8
七、内存管理
> 内存地址空间分布
○ 栈:保存局部变量,栈空间有限,且出了作用域自动销毁(函数栈帧),高地址向低地址生长
○ 堆:动态内存分配的空间,malloc,free,注意内存泄漏
○ 静态区(数据段):保存全局变量和static静态变量,生命周期伴随整个程序结束
○ 代码段:保存可执行的代码和只读常量
> malloc
○ malloc函数对返回值类型的强制转换,以及判断是否申请成功
§ (void*)malloc(int size)
○ malloc盛情0字节内存不会返回NULL指针
○ malloc申请的是虚拟内存,不是直接申请物理内存
> free
○ free只负责将内存释放,并不负责将指针置NULL,如果没有手动将指针置NULL,就会成为野指针,内存会泄漏,这个内存不能再次被使用
> malloc和free次数一致,成套使用
int *pp;
pp = (int*)malloc(sizeof(int) * 10);
malloc必须经过计算得到初始化的值,并且得强转类型
八、函数调用栈帧过程
1. 创建栈帧初始化:push ebp,sub esp xxx,压寄存器,完成初始化
2. 创建局部变量,push参数(从左向右的顺序依次压入)
3. push:call指令的下一条指令的地址,一会返回使用
4. 再次挪动ebp,esp创建栈帧,取参数进行计算并将返回值放入寄存器返回
5. 销毁栈帧,pop掉寄存器,找到对应上一个函数的下一条指令的地址进行回退
· 每个函数的每次调用,都有独立的栈帧
堆和栈的关系
一般说的堆栈其实指的是栈,
1.堆需要显示申请,栈不用显示申请,系统自动完成
申请和释放成对使用
C——malloc/free
c++——new/delete
2.堆的空间比栈大,大的静态数组就在堆的位置放置,栈空间小,比较深的迭代会使得栈的空间内存急剧缩小。
3.栈的生命周期较短,一般在函数结束,本函数的栈也就结束使用,堆看具体情况,什么时候生命周期结束,函数的栈就退出
九、语法细节
v volatile关键字
○ volatile关键字解决编译器优化问题
○ 保证对该变量的修改都是直接对内存存放的值操作
v sizeof关键字
○ 计算括号内的字节大小,不修改值本身
v 大端和小端
○ 小端:高高/低低(高字节数据存储在高地址中,反之....)
○ 大端:高低/低高(高字节数据存储在低地址中,反之....)
○ 判断当前系统是大端还是小端??
○ 解决的问题不同:小端可以避免类型转换时内容变换,不需要调整地址
大端可以清楚的看到符号位一直在高位,看清正负
大小端的判断代码
v 头文件包含
○ #include<>:表示预处理到系统规定的路径中去获得该文件
○ #include"":表示先在当前目录找该文件,找不到再到规定目录寻找
v #pragma 预处理
○ #pragma once:保证头文件只被包含一次
○ #pragma pack():设置默认对齐数
v 逗号表达式
○ 整个表达式都会进行运算,但是最终的结果是最后的表达式运算结果
十、C语言/c++中struct结构体的定义和使用
C语言
○ struct A{
int aa;
int _a;
}a;
§ 在实例化的时候,这种方法需要带上struct定义结构体对象
§ struct A aaa;
§ 这里的a是定义了类A和类A的对象实例a
○ typedef struct B{
int bb;
int _b;
}b;
§ 在实例化的时候,这种方法不需要带上struct,typedef相当于是定义了类B的别名是b
§ b bbb;
§ b是类B的别名,b相当于是struct B
○ 如果不写类名也可以
typdef struct {
int a;
}Stu;
§ 不写类名就不能struct xxx xx;可以使用别名进行实例化,Stu s;
○ typedef struct{
int num;
int age;
}aaa;
typedef aaa bbb;
typedef aaa ccc;
上面和下面是一样的;
typedef struct{
int num;
int age;
}aaa,bbb,ccc;
这里的bbb,ccc相当于aaa的别名
三者都是结构体类型
○ 在C中,struct结构体不能包含函数,C++可以
C++
○ struct Student{
int x;
int y;
};
§ C++中可以直接利用类名实例化
□ Student stu;
○ struct Student{
int x;
int y;
}Stu1;
§ 这里的Stu1是对象,可以直接Stu1.x;Stu1.y;调用结构体内的对象
○ typedef struct Student{
int x;
int y;
}Stu2;
§ Stu2是一个结构体类型
§ 实例化的时候必须Stu2 s2; s2.x;s2.y;这样访问
○ typedef struct{
int num;
int age;
}aaa,bbb,ccc;
C++中有typedef表示aaa,bbb,ccc都是结构体类型
struct{
int num;
int age;
}aaa,bbb,ccc;
如果没有typedef,则aaa,bbb,ccc是三种不同的对象
结构体typedef的用途:
1. 定义一种类型的别名,不是简单的宏替换,可以用作同时声明指针型的多个对象
char* pa,pb;//只声明了一个指针对象和一个字符变量
typedef char* PCHAR;
PCHAR pa;
PCHAR pb;
在需要大量指针声明的地方使用别名无疑是方便快捷的
2. 在旧的C语言中,声明struct新对象的时候,需要写struct+类名+xx;
有了typedef就可以省略struct,在大量使用声明定义的时候会更加方便