目录
1. 以下为WindowsNT 32位C++程序,请计算下面sizeof的值
4. 假如考虑dst和src内存重叠的情况,strcpy该怎么实现
6. 当心C++编译器的烦人的分析机制-尽可能的解释为函数声明
1. 首先分配子能够为它所定义的内存模型中的指针和引用提供类型的定义
3. STL实现者可以假定所有属于同一类型的分配子都是等价的
15. 当你在动态分配数组的时候,请使用vector和string
18. 了解如何把vector和string数据传给旧的API
24. 为包含指针的关联容器指定比较类型,而不是比较函数,最好是准备一个模板
29.使用distance和advance将容器的const_iterator转换为iterator
30. 正确理解由reserve_iteratorr的base()成员函数所产生的iterator的用法
34. 如果要删除元素,需要在remove后面使用erase
37. 使用accumulate或者for_each进行区间统计
43. 理解ptrfun && memfun && memfunref
[C++基础]
关键字与运算符
指针与引用
1. 指针存放某个对象的地址,其本身是变量(命名了的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向地址的改变和棋指向的地址中所存放的数据的改变
2. 引用就是变量的别名,从一而终,不可变,必须初始化
3.不存在指向空的值的引用,但是存在指向空值的指针
define 和 typedef 的区别
define:
1. 只是简单的字符串替换,没有类型检查
2. 是在编译的预处理阶段起作用
3. 可以用来防止头文件重复引用
4. 不分配内存,给出的是立即数,有多少次使用就进行多少次替换
typedef:
1. 有对应的数据类型,要进行判断
2. 是在编译、运行的时候起作用
3. 在静态存储区中分配空间,在程序运行过程中内存只有一个拷贝
define 和 inline 的区别
1. define:
定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全。
2. inline:
inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用额外的开销;
内联函数是一种特殊的函数,会进行类型检查;
对编译器的一种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
1. 不能存在任何形式的循环语句
2. 不能存在过多的条件判断语句
3. 函数体不能过于庞大
4. 内联函数声明必须在调用语句之前
override 和 overload
1. override 是重写(覆盖)了一个方法
以实现不同的功能,一般是用于子类在继承父类的时候,重写父类方法。
规则:
1. 重写方法的参数列表,返回值,所抛出的异常与被重写方法一致
2. 被重写的方法不能为private
3. 静态方法不能被重写为非静态的方法
4. 重写方法的访问修饰符一定要大于被重写方法的访问修饰符
2. overload 是重载,这些方法的名称相同而参数不同
一个方法有不同的版本,存在于一个类中。
规则:
1. 不能通过访问权限,访问类型,抛出的异常进行仲裁
2. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样)
3. 方法的异常类型和数目不会对重载造成影响
使用多态是为了避免在父类里大量重载引起代码臃肿且难以维护。
重写与重载的本质区别是,加入了override的修饰符的方法,此方法始终只有一个被使用的方法。
new 和 malloc
1. new分配内存失败时,会抛出bac_alloc异常,不会返回NULL;malloc分配内存失败会返回NULL。
2. 使用new操作符申请内存分配时无需指定内存块的大小,而malloc则需要显式地指出所需内存的尺寸。
3. operato new / operator delete 可以被重载,而malloc不允许重载。
4. new/delete会调用对象的构造函数/析构函数完成对象的构造/析构,malloc不会
5. malloc 和 free是C++/C语言的标准库函数,new/delete是C++的运算符
6. new操作符从自由存储区上为对象动态分配内存空间,malloc函数从堆上动态分配内存
表格
new/delete | malloc/free | |
本质属性 | 运算符 | CRT函数 |
内存分配大小 | 自动计算 | 手动计算 |
类型安全 | 是(一个int类型指针指向float会报错) | 不是(malloc类型转换成int,分配double数据类型大小的内存空间不会报错) |
两者关系 | new封装了malloc | |
其他特点 | 除了分配和释放内存还回调用构造和析构函数 | 只分配和释放内存 |
内存分配失败会抛出bad_alloc异常 | 内存分配失败时会返回null | |
返回定义时具体类型的指针 | 返回void类型指针,使用时需要类型转换 |
constexpr 和 const
const表示“只读”的语义,constexpr 表示“常量”的语义
constexpr 智能定义编译期常量,而const可以定义编译期常量,也可以定义运行期常量。
将一个成员函数标记为constexpr,则顺带也将其标记为const。如果将一个变量标记为constexpr,则也是const的。但是相反不成立。
constexpr变量
复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
必须用常量初始化:
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007;
如果constexpr声明中定义了一个指针,constexpr仅对指针有效,与对象无关。
constexpr int *p = nullptr; //常量指针 顶层const
const int *q = nullptr; //指向常量的指针, 底层const
int *const q = nullptr; //顶层const
constexpr函数
constexpr函数是指能用于常量表达式的函数。
函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
constexpr int new() {return 42;}
为了可以在编译过程中展开,constexpr函数被隐式地转换成了内联函数。
constexpr和内联函数可以在程序中多次定义,一般定义在头文件。
constexpr构造函数
构造函数不能说const,但字面值常量类的构造函数可以是constexpr。
constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用constexpr修饰
const
指针常量:const int* d = new int(2);
常量指针:int *const d = new int(2);
区别方法:
左定值,右定向:指的是const在*的左边还是右边
拓展:
顶层const:指针本身是常量;
底层const:指针所指的对象是常量;
若要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;
const 和 static 的区别
关键字 | 修饰常量【非类中】 | 修饰成员变量 | 修饰成员函数 |
const | 超出其作用域后空间会被释放; 在定义时必须初始化,之后无法改变; const形参可以接受const和非const类型的实参 |
只在某个对象的声明周期内是常量,而对整个对象而言是可变的; 不能赋值,不能在类外定义;只能通过构造函数的参数初始化列表初始化【原因:因为不同的对象对其const数据成员的值可以不同,所以不能在类中声明时初始化】 |
防止成员函数修改对象的内容【不能修改成员变量的值,但是可以访问】 const对象不可以调用非const的函数;但是非const对象可以调用; |
static | 在执行函数后不会释放其存储空间 | 只能用在类定义体内部的声明,外部初始化,且不加static | 1. 作为类作用域的全局函数【不能访问非静态数据成员和调用非静态成员函数】 2. 没有this指针【不能直接存取非类的非静态成员,调用非静态成员函数】 3. 不能声明为virtual |
const和static不能同时修饰成员函数,原因:静态函数不含有this指针,即不能实例化,而const成员函数必须具体到某一实例 |
constexpr的好处:
1. 为一些不能修改数据提供保障,写成变量则就有被意外修改的风险。
2. 有些场景,编译器可以在编译期对constexpr的代码进行优化,提高效率。
3. 相比宏来说,没有额外的开销,但更安全可靠。
volatile
定义:
[与const绝对对立的,是类型修饰符] 影响编译期编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。
作用:
指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问
使用场合:
在中断服务程序和cpu相关寄存器的定义
举例说明:
for(volatile int i=0; i<100000; i++); // 它会执⾏,不会被优化掉
extern
定义:声明外部变量【在函数或者文件外部定义的全部变量】
static
作用:实现多个对象之间的数据共享和隐藏,并且使用静态成员还不会破坏隐藏规则,默认初始化为0
前置++ 和 后置++
self &operator++() {
node = (linktype)((node).next);
return *this; }
const self operator++(int) {
self tmp = *this;
++*this;
return tmp; }
为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默认给int指定为
01. 为什么后置返回对象,而不是引用?
因为后置为了返回旧值创建了一个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,那么引用也会因为对象的销毁而销毁。
02. 为什么后置前面也要加const?
可以不加,但是为了防止使用I++++,连续两次调用后置++重载符
原因:
与内置类行为不一致;无法获得所期望的结果,因为第一次返回的是旧值,调用两次后置++,结果只累加了一次,必须手动禁止其合法化,就要在前面加上const。
03. 处理用户的自定义类型
最好使用前置++,因为它不会创建临时对象,进而不会带来构造和析构造成的额外开销。
std::atomic
问题:a++ 和 int a = b 在C++中是否是线程安全的?
答案:不是
例1:
a++:此部分C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,不是原子的。
其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的寄存器中
mov eax, dword ptr [a] # (1)
inc eax # (2)
mov dword ptr [a], eax # (3)
int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {
a++;
}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {
a++;
}
例2:
int a = b; 从C/C++语法的级别来看,这条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器中,再从该寄存器搬运到变量a的内存地址中:
mov eax, dword ptr [b]
mov dword prt [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程出现而不确定的情况。
解决办法:C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是一个模板类型:
template<class T>
struct atomic;
我们可以传入具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型
// 初始化1
std::atomic<int> value;
value = 99;
// 初始化2
// 下⾯代码在Linux平台上⽆法编译通过(指在gcc编译器)
std::atomic<int> value = 99;
// 出错的原因是这⾏代码调⽤的是std::atomic的拷⻉构造函数
// ⽽根据C++11语⾔规范,std::atomic的拷⻉构造函数使⽤=delete标记禁⽌编译器⾃动⽣成
// g++在这条规则上遵循了C++11语⾔规范。
C++三大特性
访问权限
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
在类的内部(定义类的代码内部),无论成员被声明为public,protected还是private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public成员,不能访问private、protected属性的成员。
无论公有继承、私有和保护继承,私有成员不能被“派生类”访问,基类中的公有和保护成员能被“派生类”访问。
对于公有继承,只有基类中的公有成员能被“派生类”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派生类对象”访问。
1. 继承
定义:
让某种类型对象获得另一个类型对象的属性和方法
功能:
它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展
常见的继承有三种方式:
1. 实现继承:指使用基类的属性和方法而无需额外编码的能力
2. 接口继承:指仅使用属性和方法的名称,但是子类必须提供实现的能力
3. 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力
例如:
2. 封装
定义:
数据和代码捆绑在一起,避免外界干扰和不确定性访问;
功能:
把客观事务封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法用private修饰。
3. 多态
定义:
同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
功能:
多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给他的子对象的特性以不同的方式运作。
简单概括:允许将子类类型的指针赋值给父类类型的指针。
实现多态的两种方式:
1. 覆盖(override):指子类重新定义父类的虚函数的做法。
2. 重载(overload):指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许都不同)
例如:
基类是⼀个抽象对象——⼈,那学⽣、运动员也是⼈,⽽使⽤这个抽象对象既可以表示学⽣、也可以表示运动员。
虚函数
当基类希望派生类定义适合自己的版本,就将这些函数声明为虚函数(virtual)。
虚函数依赖虚函数表工作,表来保存虚函数的地址,当我们用基类指针指向派生类时,虚表指针指向派生类的虚函数表。
这个机制可以保证派生类中的虚函数被调用到。
1. 虚函数是动态绑定的
也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数,这是虚函数的基本功能。
2. 多态(不同继承关系的类对象,调用同一函数产生不同行为)
1. 调用函数的对象必须是指针或者引用
2. 被调用的函数必须是虚函数(virtual),且完成了虚函数的重写(派生类中有一个跟基类的完全相同虚函数)
3. 动态绑定绑定的是动态类型
所对应的函数或属性依赖于对象的动态类型,发生在运行期。
4. 构造函数不能是虚函数
而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己的还没有构造好,多态是被disable的。
5. 虚函数的工作方式
依赖虚函数表工作的,表来保存虚函数地址,当我们用基类指针指向派生类时,虚表指针vptr指向派生类的虚函数表。这个机制可以保证派生类中的虚函数被调用到。
6. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
7. 将一个函数定义为纯虚函数
实际上是将这个类定义为抽象类,不能实例化对象;纯虚函数通常没有定义体,但完全可以拥有。
8. inline,static,constructor三种函数都不能带virtual关键字。
(1)inline是在编译时展开必须要有实体;
内联函数是指在编译期间用被调用函数体本身来代替函数的调用指令,但虚函数的多态性需要在运行时根据对象的类型才知道调用哪个虚函数,所以没法在编译时进行内联函数展开。
(2)static属于class自己的类相关,必须有实体;
static成员没有this指针。virtual函数一定要通过对象来调用,有隐藏的this指针,实例相关。
9. 析构函数可以是纯虚的
但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
10. 派生类的override虚函数定义必须和父类完全一致。
除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。
为什么要虚继承?
1. 为了解决多继承命名冲突和冗余数据问题
C++提出了虚继承,使得派生类中只保留一份间接基类的成员。其中多继承(Multiple inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。
2. 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类
其中,这个被共享的基类就称为虚基类(Virtual Base Class),其中A就是一个虚基类。在这种机制下,不论虚基类A在继承体系出现了多少次,在派生类中都只包含一份虚基类的成员。
类A有一个成员变量a,不使用虚继承,那么在类D中直接访问a就会产生歧义。
编译器不知道它究竟来自哪个路径。
C++标准库中的iostream类就是一个虚继承的实际应用案例。
iostream从istream和ostream直接继承而来,而istream和ostream又都继承自一个共名的baseios的类,是典型的菱形继承。
此时istream 和 ostream 必须采用虚继承,否则将导致iostream类中保留两份baseios类的成员。
使用多继承经常出现二义性,必须小心;
一般只有在比较简单和不易出现二义性或者实在必要的情况下才使用多继承,能用单一继承解决的问题就不用多继承。
空类
1. 为何空类的大小不是0
为了确保两个不同对象的地址不同,必须如此。
类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。
同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以空类的sizeof是1,而不是0。
何时空想虚函数地址表:
如果派生类继承的第一个是基类,且该基类定义了虚函数的地址表,则派生类就共享该表首地址占用的存储单元。
对于除前述情形以外的其他任何情形,派生类在处理完所有基类或者虚类后,根据派生类是否建立了虚函数地址表,确定是否为该表首址分配内存单元。
class X{}; //sizeof(X):1
class Y : public virtual X {}; //sizeof(Y):4
class Z : public virtual X {}; //sizeof(Z):4
class A : public virtual Y {}; //sizeof(A):8
class B : public Y, public Z{}; //sizeof(B):8
class C : public virtual Y, public virtual Z {}; //sizeof(C):12
class D : public virtual C{}; //sizeof(D):16
抽象类与接口的实现
接口描述了类的行为和功能,而不需要完成类的特定实现;C++接口是使用抽象类来实现的
1. 类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用“=0”来指定的。
2. 设计抽象类(通常称为ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,只能作为接口使用。
class Shape
{
public:
// 提供接⼝框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派⽣类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
//主函数:
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
Rect.getArea(); //35
Tri.setWidth(5);
Tri.setHeight(7);
Tri.getArea(); //17
智能指针
1. shared_ptr
1. shared_ptr的实现机制是在拷贝构造时使用同一份引用计数
(1)一个模板指针T* ptr
指向实际的对象
(2)一个引用次数
必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete
(3)重载operator* 和 operator ->
使得能像指针一样使用shared_ptr
(4)重载copy constructor
使其引用次数加一(拷贝构造函数)
(5)重载operator=(赋值运算符)
如果原来的shared_ptr已经由对象,则让其引用次数减一并判断引用是否为零(是否调用delete),然后将新的引用次数加一
(6)重载析构函数
使引用次数减一并判断引用是否为零;(是否调用delete)
2. 线程安全问题
(1)同一个shared_ptr被多个线程“读”是安全的;
(2)同一个shared_ptr被多个线程“写”是不安全的
证明:在多个线程中同时对一个shared_ptr循环执行两边swap。shared_ptr的swap函数的作用就是和另一个shared_ptr交换引用对象和引用计数,是写操作。执行两遍swap后,shared_ptr引用对象的值应该不变
(3)共享引用计数的不同的shared_ptr被多个线程“写”是安全的。
2. unique_ptr
1. unique_ptr“唯一”拥有其所指对象
同一时刻只能有一个unique_ptr指向给定对象,离开作用域时,若其指向对象,则将其所指对象销毁(默认delete)。
2. 定义unique_ptr时
需要将其绑定在一个new返回的指针上。
3. unique_ptr不支持普通的拷贝和赋值(因为拥有所指对象)
但是可以拷贝和赋值一个将要被销户的unique_ptr;可以通过release或者reset将指针所有权从一个非const的unique_ptr转移到另一个unique。
3. weak_ptr
1. weak_ptr是为了配合shared_ptr而引入的一种智能指针
它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况,但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
2. 和shared_ptr指向相同内存
shared_ptr析构之后内存释放,在使用之前使用挂表示公有lock()检查weak_ptr是否为空指针。
C++强制类型转换
关键字:static_cast 、dynamic_cast、reinterpret_cast 和 const_cast
1. static_cast
没有运行时类型检查来保证转换的安全性
进行上行转换(把派生类的指针或引用转换成基类的表示)是安全的
进行下行转换(把基类的指针或引用转换为派生类的表示),没有动态类型检查,是不安全的。
使用:
1. 用于基本数据类型之间的转换,如把int转换成char。
2. 把任何类型的表达式转换成void类型。
2. dynamic_cast
在下行转换时,dynamic_ptr具有类型检查(信息在虚函数中)的功能,比static_cast更安全。
转换后必须是类的指针、引用或者void*,基类要有虚函数,可以交叉转换。
dynamic本身只能用于存在虚函数的父子关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。
3. reinterpret_cast
可以将整型转换为指针,也可以把指针转换为数组;可以在指针和引用里进行肆无忌惮的转换,平台移植性价比差。
4. const_cast
常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性。
C++内存模型
字符串操作函数
常见的字符串函数实现
1. strcpy()
把从strsrc地址开始且含有“\0”结束符的字符串复制到从strdest开始的地址空间,返回类型为char*
char *strcpy(char *strDest, const char *strSrc)
{
assert((strDest != NULL) && (strSrc != NULL));
char *address = strDest;
while((*strDest++ = *strSrc++) != '\0');
return address;
}
2. strlen()
计算给定字符串的长度。
int strlen(const char *str)
{
assert(str != NULL);//断言字符串地址非0
int len;
while((*Str++) != '\0')
{
len++;
}
return len;
}
3. strcat()
作用是把src所指字符串添加到dest结尾处。
char *strcat(char *dest, const char *src)
{
assert(dest && src);
char *ret = dest;
//找到dest的'\0'结尾符
while(*dest)
{
dest++;
}
//拷贝(while循环退出时,将结尾符'\0'也做了拷贝)
while(*dest++ = *src++) {}
return ret;
}
4. strcmp()
比较两个字符串设这两个字符串为str1, str2,
若str == str2,则返回零
若str1 < str2,则返回负数
若str1 > str2,则则返回正数
int strcmp(const char *str1, const char *str2)
{
assert(str1 && str2);
//找到首个不相等的字符
while(*str1 && *str2 && (*str1 == *str2))
{
str1++;
str2++;
}
return *str1 - *str2;
}
内存泄漏
1. 什么是内存泄漏?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
可以使用Valgrind,mtrace进行内存泄漏检查。
2. 内存泄漏的分类
(1)堆内存泄漏(Heap leak)
堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这款亚欧内存将不会被使用,就会产生heap leak。
(2)系统资源泄漏(Resource Leak)
主要指程序使用系统分配的资源比如Bitmap, handle, SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
(3)没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄漏。
3. 什么操作会导致内存泄漏?
指针指向改变,未释放动态分配内存。
4. 如何防止内存泄漏?
将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使用智能指针
5. 智能指针的了解有哪些?
智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的。C++标准中放在<memory>头文件。包括:共享指针,独占指针,弱指针
6. 构造函数,析构函数要设为虚函数吗,为什么?
(1)析构函数
需要。当派生类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调用基类虚构函数,导致派生类资源无法释放,造成内存泄漏。
(2)构造函数
不需要。没有意义。虚构函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。要创建一个对象,需要知道对象的完整信息。特别是,需要知道要创建的确切类型。因此,构造函数不应该被设定为虚函数。
测试题目
1. 以下为WindowsNT 32位C++程序,请计算下面sizeof的值
char str[] = "hello";
char *p = str;
int n = 10;
//请计算
sizeof(str) = ?; //6,是数组的所占内存的⼤⼩包括末尾的 '\0'
sizeof(p) = ?; //4, p为指针变ᰁ,32位系统下⼤⼩为 4 bytes
sizeof(n) = ?; //4,n 是整型变ᰁ,占⽤内存空间4个字节
void Func(char str[100])
{
// 请计算
sizeof(str) = ?; //4,函数的参数为字符数组名,即数组⾸元素的地址,⼤⼩为指针的⼤⼩
}
void* p = malloc(100);
// 请计算
sizeof(p) = ?; //4,p指向malloc分配的⼤⼩为100 byte的内存的起始地址,sizeof(p)为指针的⼤⼩,⽽不是它指向内存的⼤⼩
2. 分析下面Test函数会有什么样的结果
void GetMemory1(char* p)
{
p = (char*)malloc(100);
}
//程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的
//str⼀直都是NULL。strcpy(str, "hello world"),将使程序奔溃
void Test1(void)
{
char* str = NULL;
GetMemory1(str);
strcpy(str, "hello world");
printf(str);
}
char *GetMemory2(void)
{
char p[] = "hello world";
return p;
}
//可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,
//使其原现的内容已经被清除,新内容不可知。
void Test2(void)
{
char *str = NULL;
str = GetMemory2();
printf(str);
}
void GetMemory3(char** p, int num) {
*p = (char*)malloc(num);
}
//能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
void Test3(void)
{
char* str = NULL;
GetMemory3(&str, 100);
strcpy(str, "hello");
printf(str);
}
//篡改动态内存区的内容,后果难以预料。⾮常危险。
//因为 free(str);之后,str成为ᰀ指针,if(str != NULL)语句不起作⽤。
void Test4(void)
{
char *str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
cout << str << endl;
}
}
3. 实现内存拷贝函数
char* strcpy(char *dst, const char *src) //[1]
{
assert(dst != NULL && src != NULL); //[2]
char *ret = dst; //[3]
while((*dst++ = *src++) != '\0'; //[4]
return ret;
}
[1] const修饰:
源字符串参数用const修饰,防止修改源字符串。
[2] 空指针检查:
(1)不检查指针的有效性,说明不注重代码的健壮性。
(2)检查有效性时使用assert(!dst && !src); char*转换为bool即是类型隐式转换,这种功能虽然灵活,但是更多的是导致出错率增大和维护成本提高。
(3)检查指针有效性时使用assert(dst != 0 && src != 0); 直接使用常量会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器会检查出来。
[3] 返回目标地址
忘记保存原来的strdst值。
[4]'\0'
(1)循环写成while(*dst++=*src++);
(2)循环写成while (*src!='\0') *dst++ = *src++; 循环结束后没有正确加上'\0'
(3)返回dst的原始值使函数能够支持链式表达式
4. 假如考虑dst和src内存重叠的情况,strcpy该怎么实现
char s[10]="hello";
strcpy(s, s+1);
// 应返回 ello
strcpy(s+1, s);
// 应返回 hhello 但实际会报错
// 因为dst与src᯿叠了,把'\0'覆盖了
所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有一种情况:src<=dst <= src+strlen(src)
C函数memcpy自带内存重叠检测功能。
char * strcpy(char *dst,const char *src) {
assert(dst != NULL && src != NULL);
char *ret = dst;
my_memcpy(dst, src, strlen(src)+1);
return ret; }
/* my_memcpy的实现如下 */
char *my_memcpy(char *dst, const char* src, int cnt) {
assert(dst != NULL && src != NULL);
char *ret = dst;
/*内存᯿叠,从⾼地址开始复制*/
if (dst >= src && dst <= src+cnt-1)
{
dst = dst+cnt-1;
src = src+cnt-1;
while (cnt--)
{
*dst-- = *src--;
}
}
else //正常情况,从低地址开始复制
{
while (cnt--)
{
*dst++ = *src++;
}
}
return ret; }
5. 按照下面要求写程序
已知string的原型为:
class String
{
public:
String(const char *str = NULL);
String(const String &other);
~ String(void);
String & operate =(const String &other);
private:
char *m_data;
};
// 构造函数
String::String(const char *str) {
if(str==NULL)
{
m_data = new char[1]; //对空字符串⾃动申请存放结束标志'\0'
*m_data = '\0';
}
else
{
int length = str