C++总复习(一)
C++总复习
1.虚拟地址空间布局
(1)保留区的作用?
保留区的目的是不可访问的区域(不允许读写),128兆大小,放入的是c的库,C语言的基本库,操作系统基本函数(操作系统调用)
(2)命令行参数:
int main(int argc,char *argv[],…)
从屏幕中获取命令
(3)共享库的位置为什么不在保留区?(保留区不可修改)
库是提供声名的地方,为什么不放入保留区?原因只有一个:保留区的大小太小,共享库太大;
(4)堆和栈的区别?
1)虚拟内存空间布局
(管理方式,生长方向,空间大小,分配方式,分配效率,分配后的系统响应,碎片问题)
**管理方式:**栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露;
空间大小:(栈的大小一般是2M到10M,ulimit -s 查看系统栈内存大小)空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
生长方向:
栈是从高地址向低地址扩展的连续内存,内存都是连续的,速度快,但是程序不可控;
堆的内存是从低地址向高地址扩展的,但是内存不一定连续,开辟的效率低一点,容易产生内存碎片,灵活高;系统用链表存储空闲地址空间,自然不可连续,而链表就是从低地址向高地址遍历的;
分配方式:
栈内存是由系统分配,系统释放的;以函数为单位进行栈内存分配,函数栈帧,局部变量,形参变量等都存放在栈内存上。堆内存是由用户自己分配的,C 语言用 malloc/free进行分配释放,C++用 new/delete 进行分配释放,由于堆需要用户自己管理,因此堆内存很容易造成内存泄露,而栈内存不会。
分配效率:
栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。
分配后的系统响应:
只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。
操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链 表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功你能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。
内存碎片问题:
栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。
而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用 也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆 。 使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。
2)数据结构
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
2.编译链接原理
(1)预编译 .c/.cpp->.i
1)展开宏,文本替换
2)处理头文件(方法的定义点),递归展开的
3)处理预编译指令,#if 0 #endif
4)删除注释
5)添加行号和标识
6)保留#pragma 预编译无法处理,交给编译器处理
(2)编译 .i->.s
1)词法分析
将源程序扫描,对代码进行标记(eg:数字不能作为命名的开头)
关键词、标识符、数字、字符串、加减乘除等于等等;
2)语法分析
建立语法树
3)语义分析
结合上下文简历语法树
4)代码优化
使用一些合适的寻址方式,使用位移代替乘法,删除多余的指令—效率高
整个过程是机器在操作—不可控,会出现错误
5)生成汇编代码(指令)
(3)汇编 .s->.o
翻译汇编代码
生成ELF格式的文件
1.没有处理强弱符号?
对于C/C++而言,编译器默认函数和已初始化的全局变量为强符号,而未初始化的全局变量为弱符号,在编程者没有显示指定时,编译器对强弱符号的定义会有一些默认行为,同时开发者也可以对符号进行指定,使用"attribute((weak))"来声明一个符号为弱符号。
定义一个相同的变量,当两者不全是强符号时,gcc在编译时并不会报错,而是遵循一定的规则进行取舍:
- 当两者都为强符号时,报错:redefinition of ‘xxx’
- 当两者为一强一弱时,选取强符号的值
- 当两者同时为弱时,选择其中占用空间较大的符号,这个其实很好理解,编译器不知道编程者的用意,选择占用空间大的符号至少不会造成诸如溢出、越界等严重后果。
2.符号表的生成(有部分符号表放入了und区域,生成了外部符号)
3.而程序的入口地址也是不确定的,虚假地址和虚假偏移
(4)链接 处理汇编遗留下来的问题
1)强弱符号的处理–合并符号表
2)符号解析(处理虚假地址和虚假偏移)
3)分配地址和空间
程序—虚拟地址的映射
4)符号的重定位
虚拟地址–物理地址
(5)运行
1)创建虚拟地址空间和内存映射(PCB)
分页机制和分段机制(创建页目录,页表将程序加载到内存中去)
2)加载指令和数据
3)程序的入口地址,写入下一行指令寄存器
3.C/C++区别-----函数符号生成规则
c:和函数名相关
c++:函数返回值+函数名+函数形式列表
规则:
int Sum(int a,int b); int : H double : N
?Sum(函数名)@@YA(调用约定)H(函数返回值)HH(函数形式参数)@z
4.c/c++区别-----函数重载
c语言无函数重载
c++函数函数重载的三要素?
1.函数同名
同名函数有三种关系:重定义(重载)、隐藏、重写(覆盖)
2.不同的参数列表
3.同一作用域
5.内联函数
在调用点直接把代码展开,无开栈清栈的开销,效率高
缺点:代码目标,有递归和循环不得使用(函数的堆栈开销《函数执行开销 建议)
以空间换取时间
inline函数都写在。h文件中,方便找到定义点(只在relaease版本生效,debug和普通函数想同)
1)与普通函数的区别:
在调用点直接把代码展开,无开栈清栈的开销,效率高
inline只在本文件中可见
2)与static函数的区别:
static修饰的函数会把符号从globe变成local(都在本文件中可见)
3)与宏的区别
宏的处理在预编译阶段,无类型检查和安全检查
内联的处理实在编译阶段处理,有类型和安全检查
6.c++引用
引用是变量的别名,
底层:以指针方式实现
1.引用一定要进行初始化
2.引用不能改变
3.引用是不能使用被引用的数据和不能被取地址的数据
4.引用变量只能使用所引用的内存单元
7.malloc/new的区别
1.new是c++开辟内存使用
malloc是c开辟内存使用
2.new是一个关键字,开辟的大小由系统计算
malloc是一个函数原型:void *malloc(size_t) 形参的大小由sizeof人为计算
3.malloc返回的是void类型,不安全(void是一个半开半闭的区间,通过类型强转后得到大小,不安全)
new可以使用类型接收 安全
4.malloc不能对堆进行初始化操作
new可以对堆内存进行初始化
5.new对数组直接传入的的是数组的个数即可
malloc则要仅算出数组整体的带下
6.开辟内存空间的位置
malloc开辟的是堆的内存
new开辟的是自由空间存储区(可以在堆上)
7.开辟失败的反馈
new抛出异常(bad_alloc) malloc返回值为NULL
8.new底层是以operator new实现的
在对象中可以进行函数重载(内存池的实现),malloc不允许
8.const的用法
1.c语言 const修饰的变量–变成常变量
在编译阶段查看是否做左值? 做–报错
2.c++ const修饰的变量为–常量
编译器:把用到常量的地方替换成敞亮初始化的值
1)常量必须做初始化操作
2)常量不允许被修改(间接修改和直接修改)
3.用法
1)修饰全局变量–变量符号从globe变成local只在本文件可见
2)修饰局部变量–变成常量
3)修饰成员变量–不依赖对象 在类外初始化
常方法–const TYPE* const this
常对象只能调用常方法
9.c/c++相互调用:
c++调用c
extern “c”{ c语言规则的代码 }
c语言调用c++
区分:所有c++源文件中自带宏_Cplusplus,以此来识别是c或者c++文件
10.面向对象编程oop思想
1.实体(属性和行为)—》ADT(抽象说明)(属性和行为)–》转换为类(类变量和属性)–》抽象成对象(对象和行为)
2.c++三大特性
封装:(该让你看到的让你看到 不让你看到的不让你看)
隐藏了对象属性和实现细节,对外提供共有接口供函数调用
pubic:都能看到 protected:本类和子类 private:本类
3.this指针
调用了this call调用约定
隐藏参数this指针,const类类型
Student *const this 确定那个对象调用的函数
11.类中的默认函数(以string类为例)
1.构造函数
1.开辟内存
2.初始化对象所占的内存空间
特点:可重载
不依赖对象调用
人为提供:系统就不会提供(提供的是不带参数构造函数)
2.析构函数
特点:不可重载
依赖对象(释放资源——调用析构)
释放空间
3.拷贝构造函数
已存在的对象去生成相同类型的新对象
string (const string& rhs)
为什么里面是引用?避免对象重复生成、栈溢出
默认的拷贝构造函数是浅拷贝,释放资源时会释放同一片内存,系统崩溃
(有成员变量指针存在就要考虑深拷贝的实现)
浅拷贝是共享资源 深拷贝是独立资源
4.赋值运算符重载函数
用已生成的对象去生成相同类型的已存在的对象
if(this!=&rhs) {
//处理 要删除delete[] mptr;//释放旧的资源
}
5.取地址运算符的重载函数
string* operator&()
{
return this;
}
6.const修饰的取地址运算符的重载函数
const string* operator&()const
{
return this;
}
12.static关键字
1.static修饰全局变量和局部变量
全局变量属于数据,不是放在.data 就是在.bss 如果是常量字符串放在.rodata段
static修饰是的全局变量从globe变成local。静态函数不能被其它文件所用;其它文件中可以定义相同名字的函数,不会发生冲突;
函数中普通局部变量在栈中开辟内存,stattic修饰后在全局数据区分配内存,使得其作用域变长(它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体外面使用它(局部变量在栈区,在函数结束后立即释放内存);),静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化
静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化);
2.static修饰普通函数
本文件可见 其他文件不可见 限制了其作用域 同时符号从globe变成loacl
3.static修饰成员变量和成员方法
修饰成员变量
改变了其生存周期 不再依赖对象 在类外初始化 不在使用this call调用约定‘
静态数据成员被当作是类的成员,由该类型的所有对象共享访问,对该类的多个对象来说,静态数据成员只分配一次内存。静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。
使用的时候要加以类的作用域,不属于对象 属于类
修饰成员方法
改变了其生存周期 不再依赖对象 在类外初始化 不在使用this call调用约定
不能访问普通成员变量和方法 在类外实现
13.模板(函数模板和类模板)
模板编译阶段:
1.定义点:编译了模板的头部
2.在调用点编译了模板实例化的头部
1.函数模板
template(typename T)
T Sum(T a,T b)
{
return a+b;
}//函数模板
int Sum(inyt a,int b)
{
return a+b;
}//模板函数
从函数模板变成模板函数是模板的实例化过程
模板的实参推演:
系统会识别传递的参数,但是会有一定的限制条件 所有均要满足定义点
(不能产生二义性 要用实参)
模板特例化(全特化和局部特例化)–char*类型 针对
模板重载(普通函数版本 模板版本 模板的特例化)
调用先后顺序(1》3》2)
问题:为什么要把模板的整个定义写在一个.h文件中?
1)链接的时候,需要实例化模板,这时候就需要找模板的具体实现了。假设在main函数中调用了一个模板函数,这时候就需要去实例化该类型的模板。注意main函数里面只包含了.h文件,也就是只有模板的声明,没有具体实现。就会报错。
2)而模板的实现.cpp里面,虽然有模板的具体实现,但是没有谁在该.cpp里面使用一个模板函数,就不会生成一个具体化的实例。
ps:模板是在需要的时候,才会去生成一个具体化的实例的,比如,你只要一个int型的实例,模板就只会给你生成一个int型的实例,模板本身是不会被执行的(也就是模板本身不产生汇编指令),是模板生成的具体化实例才产生指令(这个实例是隐藏的,我们是看不到的)