文章目录
1. C++和C的区别
参考回答:
-
设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言 -
语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
2. 四种cast转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
-
const_cast
用于将const变量转为非const,例如下面语句是编译不通过的:string str = "hello"; char *_const = str.substr(0,3).c_str();//c_str()返回const char*类型,直接赋值给char *显然出错,这句话编译不能通过
但是可以在该语句上添加const_cast即可:
char *non_const = const_cast<char *> (str.substr(0,3).c_str());//将const属性移除,可以通过编译了
-
static_cast
- 内置类型的转换,类似于C语言中的强制类型转换,在编译时期进行转换,例如:
double dValue = 12.12; float fValue = 3.14; // VS2013 warning C4305: “初始化”从“double”到“float”截断 int nDValue = static_cast<int>(dValue); // 12 int nFValue = static_cast<int>(fValue); // 3
- 用于自定义类时,静态转换会判断转换类型之间的关系,如果转换类型之间没有任何关系,则编译器会报错,不可转换;
class A{}; class B : public A{}; class C{}; void main(){ A *pA = new A; B *pB = static_cast<B*>(pA); // 编译不会报错, B类继承于A类 pB = new B; pA = static_cast<A*>(pB); // 编译不会报错, B类继承于A类 C *pC = static_cast<C*>(pA); // 编译报错, C类与A类没有任何关系。error C2440: “static_cast”: 无法从“A *”转换为“C *” }
- 把void类型指针转为目标类型指针(不安全)。
-
dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
注意:
(1)dynamic_cast是运行时处理的,运行时要进行类型检查,而其他三种都是编译时完成的;
(2)不能用于内置基本数据类型间的强制转换;
(3)使用dynamic_cast进行转换时,基类中一定要有虚函数,否则编译不通过;(4)dynamic_cast转换若成功,返回的是指向类的指针或引用;若失败则会返回NULL;
(5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。向下转换的成败取决于将要转换的类型,即要强制转换的指针所指向的对象实际类型与将要转换后的类型一定要相同,否则转换失败。 -
reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
2.1 为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
3. C/C++ 中指针和引用的区别?
参考回答:
- 指针内部存储所指对象的地址,因此其在占据一定的内存;而引用只是一个变量的别名,其不占用内存;
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 指针在创建时可以直接初始化,也可以随后在使用的时候初始化,或者将其初始化为NULL;而引用必须被初始化且必须是一个已有对象的引用,且一旦定义为某个对象的引用后就不再作为其他对象的引用;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 可以有const指针,但是没有const引用;
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 指针可以有多级指针(**p),而引用至于一级;
- 指针和引用使用++运算符的意义不一样,指针++可以用于操作数组的下一位,引用++则是修改引用对象的数值;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
4. 四个智能指针smart pointer
C++中共有四个智能指针:shared_ptr, unique_ptr, weak_ptr, auto_ptr
4.1 auto_ptr
auto_ptr是c++98中的智能指针,在c++11中已经被遗弃,取而代之是三个智能指针shared_ptr, unique_ptr, weak_ptr。
auto_ptr 是C++标准库提供的类模板,auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者。当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有的动态内存自动释放。即使发生异常,通过异常的栈展开过程也能将动态内存释放。auto_ptr不支持new 数组。
auto_ptr被代替的原因如下:
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
上述代码在编译时不会报错,但是运行的时候会出错。但是,p2剥夺了p1的所有权,所以当程序运行时如果再次访问p1将会报错,或者当程序退出时,自动释放p1和p2所指对象时会出错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
4.1.1 auto_ptr与new/delete的区别:
优势:
- auto_ptr创建的是一个对象,而new与delete只是函数!使用auto_ptr就不会因为忘了delete掉而出现内存泄漏。
缺点:
- 不能用来表示new创建的数组
- 存在潜在的内存崩溃问题
4.2 unique_ptr
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用,是auto_ptr的升级版本,在使用时可以避免内存崩溃现象的发生:
unique_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
unique_ptr<string> p2;
p2 = p1; //unique_ptr会报错.
上述代码在编译的过程中会报错,因此避免了auto_ptr的缺陷,比auto_ptr更智能。
当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
最后一句话中unique_ptr<string>(new string ("You"))
可以理解为是一个临时的unique_ptr指针,此时允许作为右值进行操作。
4.3 shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的。如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
int i;
A(int n):i(n) { };
~A() { cout << i << " is destructed" << endl; }
};
int main()
{
shared_ptr<A> sp1(new A(2)); //A(2)由sp1指向,
shared_ptr<A> sp2(sp1); //A(2)同时交由sp2指向
shared_ptr<A> sp3;
sp3 = sp2; //A(2)同时交由sp3指向
cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl;
int count = sp3.use_count(); // 此时sp1、sp2、sp3共享A(2)
cout << "There are " <<count << "ptrs sharing the same variable" << endl;
A * p = sp3.get(); // get返回指向的指针,p 指向 A(2)
cout << p->i << endl; // 输出 2
sp1.reset(new A(3)); // reset导致指向新的指针, 此时sp1指向A(3)
sp2.reset(new A(4)); // sp2指向A(4)
cout << sp1->i << endl; // 输出 3
sp3.reset(new A(5)); // sp3指向A(5), 此时指向A(2)的所有指针都被销毁,因此A(2)自动被delete
cout << "End of code" << endl; // 此时程序全部运行完,代码运行结束后系统自动释放sp1、sp2、sp3指针所指向的内容
return 0;
}
运行结果如下:
2,2,2
There are 3ptrs sharing the same variable
2
3
2 is destructed
End of code
5 is destructed
4 is destructed
3 is destructed
4.3 weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
运行结果如下:
2
2
由运行结果可以看出, fun函数中pa,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用)。
如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();
5. 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
造成野指针的原因有以下几点:
- 指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。 - 指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。 - 指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。示例程序如下:
函数 Test1 在执行语句 p->Func()时,p 的值还是 a 的地址,对象 a 的内容已经被清除,所以 p 就成了“野指针” 。class A { public: void Func(void){ cout << “Func of class A” << endl; } }; class B { public: A *p; void Test(void) { A a; p = &a; // 注意a的生命期 ,只在这个函数Test中,而不是整个class B } void Test1() { p->Func(); // p 是“野指针” } };
6. 返回局部变量
通常情况下,局部变量在函数调用结束后便被销毁,因此不能直接返回局部变量,可以通过以下几种方式返回:
- 返回指向字符串常量的指针
字符串常量是存储在文字常量区,其在程序结束时由系统释放,因此只要返回指向该字符串常量的指针,即可以在调用函数外使用。 - 不能返回以局部变量方式创建的字符串数组首地址
字符串数据属于局部变量,其在调用函数结束后会被释放,因此返回的指向该数组的指针会变成野指针,因此该方法不可用。 - 允许返回局部变量的值,不允许返回局部变量的地址
通常使用的如下所示的函数,其返回的不是局部变量,也不是局部变量的地址,而是局部变量的值,因此在函数调用结束时,前两者都会被系统释放。int func() { int a=0; return a; }
- 如果函数的返回值非要是一个局部变量的地址,那么该局部变量一定要申明为static类型
使用static声明的局部变量的生命周期是整个程序,因此在函数调用结束时,该static变量的内存地址仍然被保留,可以通过返回指向该变量的指针,使得函数调用结束后还可以访问该变量。 - 可以返回指向堆内存的指针
堆内的数据是由程序员自己申请、释放的,因此可以在调用函数里面动态申请变量,在函数调用结束时不主动释放所申请的内存空间,就可以在函数调用结束后继续在其他地方使用该变量。
7. 为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
8. 函数指针
函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样。
8.2 用途
调用函数和做函数的参数,比如回调函数。
8.3 示例
char * fun(char * p) {…} // 函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun
pf(p); // 通过函数指针pf调用函数fun
9. fork函数
Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。
最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。
10. 析构函数
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
构造函数顺序:1)基类的析构造函数;2)对象成员构造函数;3)派生类的构造函数。
#include <iostream>
#include <memory>
using namespace std;
#include<iostream>
using namespace std;
class CBase {
public:
CBase() {
cout << "CBase():Constructor" << endl;
}
~CBase() {
cout << "CBase(): Destructor" << endl;
}
};
class CDerive :public CBase {
public:
CDerive() {
cout << "CDerive():Constructor" << endl;
}
~CDerive() {
cout << "CDerive(): Destructor" << endl;
}
};
10.1 分析
在使用类指针指向类对象时,可以分为四种情况:
10.1.1 情景1:使用基类指针指向派生类对象
int main(int argc, char* argv[])
{
CBase* p = new CDerive(); // 使用基类指针指向派生类对象
delete p;
return 0;
}
10.1.2 情景2: 使用派生类指针指向派生类对象
int main(int argc, char* argv[])
{
CDerive* pp = new CDerive();
delete pp;
return 0;
}
10.1.3 情景3: 经过转换后使用基类指针指向派生类对象
int main(int argc, char* argv[])
{
CDerive* pp = new CDerive();
CBase * p = (CBase*)pp;
delete p;
return 0;
}
10.1.4 情景4: 经过转换后使用派生类指针指向派生类对象
int main(int argc, char* argv[])
{
CBase * p = new CDerive();
CDerive * pp = (CDerive*)p;
delete pp;
return 0;
}
情景1、3运行结果如下:
CBase():Constructor
CDerive():Constructor
CBase(): Destructor
情景2、4运行结果如下:
CBase():Constructor
CDerive():Constructor
CDerive(): Destructor
CBase(): Destructor
10.2 总结
由上面的实验结果可以看出,当 new CDerive() 时,会先运行基类的构造函数,然后再运行派生类的构造函数;
而当 delete pointer 时,编译器只考虑 pointer 指针本身的类型而不关心 pointer 实际指向的类型,即:
- 若 pointer 为基类指针,则只调用基类的析构函数(不管 pointer 实际指向的是基类还是派生类);
- 若 pointer 是派生类指针,则先调用派生类的析构函数,再调用基类的析构函数,调用顺序与调用构造函数的顺序相反。
11. 重载、覆盖、隐藏
- 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
- 重写:也叫覆盖,子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
- 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
12. 虚函数和多态
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
13. ++i和i++的实现
参考回答:
- ++i 实现:
int& int::operator++()
{
*this +=1;
return *this;
}
- i++ 实现:
const int int::operator(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}
14. 在main函数执行前先运行的函数
- 变量会在main()前运行
#include <iostream> using namespace std; int foo(void); int i=foo(); int foo(void) { cout<<"before main"<<endl; return 0; } int main(void) { cout<<"i'm main"<<endl; }
- 构造函数在main()之前运行,析构函数在main()之后运行
#include <iostream> using namespace std; __attribute((constructor))void before() { cout<< "before main"<<endl; } int main() { cout << "main() is running."<<endl; } __attribute((destructor))void after() { cout<<"after main"<<endl; }
15. 常量指针&指针常量
15.1 指针常量:
顾名思义它就是一个常量,但是是指针修饰的。因为声明了指针常量,说明指针变量不允许修改。如果指针指向一个地址,则该地址不能被修改,但是该地址里的内容可以被修改。如下代码所示:
int a,b;
int * const p=&a // 指针常量
//那么分为一下两种操作
*p=9; //操作成功
p=&b; //操作错误
可以理解为指针的常量,其本质是常量,因此其自身一旦确定就不能修改,也就是指向的地址一旦确定就不能修改,但是地址内存放的数据可以修改。
15.2 常量指针
如果在定义指针变量的时候,数据类型前用const修饰,被定义的指针变量就是指向常量的指针变量,指向常量的指针变量称为常量指针,如下面代码所示:
在这个例子下定义以下代码:
int a,b;
const int *p=&a //常量指针
//那么分为一下两种操作
*p=9;//操作错误
p=&b;//操作成功
可以理解为是常量的指针,其本质是指针,也就是指向常量的指针,因此其指向地址内的数据不能修改,但是指向的地址可以修改。
16. 以下四行代码的区别是什么?
const char * arr = "123";
char * brr = "123";
const char crr[] = "123";
char drr[] = "123";
参考回答:
const char * arr = “123”;
//字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
char * brr = “123”;
//字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
const char crr[] = “123”;
//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
char drr[] = “123”;
//字符串123保存在栈区,可以通过drr去修改
17. const修饰成员函数的目的
参考回答:
const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。