- 1. 构造函数可不可以为虚函数?
- 2. 说说拷贝构造函数?
- 3. 拷贝构造函数中的深浅拷贝区别?
- 🔥4. 为什么父类析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?
- 5. 析构函数有什么作用?
- 6. 有哪些cast转换?
- 🔥7. 理解volatile吗?
- 8. 理解explicit吗?
- 9. static关键字有什么作用?
- 10. 怎么定义常量的?常量存放在内存的哪个位置?
- 🔥11. 菱形继承是什么?会产生哪些问题?怎么解决?
- 12. 指针和引用的区别?
- 13. 指针和数组的区别?
- 14. 函数指针和指针函数的区别?
- 15. 野指针是什么?
- 16. 静态函数和虚函数的区别?
- 17. 重载和重写的区别?
- 🔥18. 怎么理解多态?
- 19. 说说类成员的访问权限?
- 20. 多态是如何实现的?
- 🔥21. 虚函数表和虚函数表指针是怎么运作的?
- 22. 智能指针有哪些?用过哪几种?
- 23. C++11新特性有哪些?了解过吗?
- 24. C++程序从编写到运行起来的整个过程是怎样的?
- 🔥25. C++程序的内存分布是怎么样的?
- 26. STL有了解过吗?说说vector和list的区别?
- 27. 在你看来,C和C++有什么区别?
- 28. typedef和define的区别是什么?
- 29. C++作为面向对象语言,他有哪些特性?
- 30. include双引号""和尖括号<>的区别?
- 31. 堆和栈的访问哪个更快?
- 32. new/delete与malloc/free的区别?
- 33. 说说你对STL的理解?
- 34. STL中迭代器的作用,指针和迭代器的区别?
- 35. map和set有什么区别,分别又是怎么实现的?
构造函数的名字和类名相同,没有返回类型,类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数量或参数类型上有所区别。
构造函数不能被声明为const,当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
如果构造函数是虚函数,则它需要通过类的虚表来调用,而虚表的指针保存在对象中,没有调用构造函数,就没有对象。这就要涉及到C++对象的构造问题了,C++对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
2. 说说拷贝构造函数?拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝。
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
3. 拷贝构造函数中的深浅拷贝区别?在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
🔥4. 为什么父类析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
5. 析构函数有什么作用?- 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
- 析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
- 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何 *** 作。所以许多简单的类中没有用显式的析构函数。
- 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
- 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
- const_cast
const_cast是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
用法:const_cast(expression)
int a = 1;
const int * ptr = a;
int *ptr2 = const_cast<int *>(ptr);//假设a在此处很难获取,即重新定义一个int*指针,来接受ptr中除了const属性之外的东西。
*ptr2 = 2; // a的值不能通过ptr来修改,但是可以通过const_cast转化后的ptr2来修改
- static_cast
用法:static_cast < type-id > ( expression )
它主要有如下几种用法:
(1)用于类层次结构中基类和派生类之间指针或引用的转换
(2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证
(3)把空指针转换成目标类型的空指针
(4)把任何类型的表达式转换为void类型
注意:
static_cast不能转换掉expression的const、volitale或者__unaligned属性。
static_cast可以实现C++中内置基本数据类型之间的相互转换。
int i;
float f =166.7f;
i = static_cast<int>(f); //i的值为166
- dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
#include "stdafx.h"
#include
using namespace std;
class Base
{
public:
Base(){};
virtual void Show(){cout<<"This is Base calss";}
};
class Derived:public Base
{
public:
Derived(){};
void Show(){cout<<"This is Derived class";}
};
int main()
{
//这是第一种情况,向下转化
Base* base = new Derived;
if(Derived *der= dynamic_cast<Derived*>(base))
{
cout<<"第一种情况转换成功"<<endl;
der->Show();
cout<<endl;
}
//这是第二种情况
Base * base1 = new Base;
if(Derived *der1 = dynamic_cast<Derived*>(base1))
{
cout<<"第二种情况转换成功"<<endl;
der1->Show();
}
else
{
cout<<"第二种情况转换失败"<<endl;
}
//第三种情况,向上转化
Base *base2;
Derived *der2 = new Derived;
//base2 = dynamic_cast (der2); //正确,但不必要。
base2 = der2; //向上转换总是安全的
base2->Show();
delete(base);
delete(base1);
delete(base2);
}
- reinterpret_cast
🔥7. 理解volatile吗?几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如: *** 作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
volatile int i=10;
int a = i;
int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过 *** 作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样一来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
8. 理解explicit吗?C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有缺省值的多参构造函数),承担了两个角色:
- 用于构建单参数的类对象
- 隐含的类型转换 *** 作符.
例如一个类A的构造函数A(int i)就是,既可以用来作为构造器,又可以实现隐式转换A a=1;因为1可以通过构造函数A(int i)转换为一个类A的对象。如果我们不想这种隐式转换出现,我们可以对构造函数进行explicit修饰。
- 关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换发生。
- 声明为explicit的构造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象。
- explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。
- 全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量,静态存储区,在整个程序运行期间一直存在
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。例如在a.c中定义了static int a=10;那么在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。
- 局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
作用域:作用域为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
- 静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
- 类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
- 类的静态函数
10. 怎么定义常量的?常量存放在内存的哪个位置?静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>)。对一个类中成员变量和成员函数来说,加了static关键字,则此变量/函数就没有了this指针了,必须通过类名才能访问。
常量在C++里的定义就是一个const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。
🔥11. 菱形继承是什么?会产生哪些问题?怎么解决?
菱形继承如上图。B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。
因此引入虚继承来解决这个问题。虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。虚基类用virtual声明继承关系就行了。
12. 指针和引用的区别?- 指针有自己的一块空间,而引用只是一个别名;
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
- 作为参数传递时,指针需要被解引用才可以对对象进行 *** 作,而直接对引用的修改都会改变引用所指向的对象;
- 可以有const指针,但是没有const引用;
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 指针可以有多级指针(**p),而引用只有一级;
- 指针和引用使用++运算符的意义不一样;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
- 函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数。
- 指针函数是一种返回值为指针的 函数。例如A *createA();声明了一个返回值为指向A类型的指针的参数列表为空的函数。
野指针就是指向一个已删除的对象或者未申请但访问受限内存区域的指针。
16. 静态函数和虚函数的区别?静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。
17. 重载和重写的区别?- 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
- 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址(实现运行时多态的方式)。使用了虚函数,会增加访问内存开销,降低效率。
#include
using namespace std;
class A{
public:
virtual void method(){
// void method(){
cout<<"in class A"<<endl;
}
void other(){
cout<<"in class A, other fun"<<endl;
}
};
class B : public A{
public:
void method(){
cout<<"in class B"<<endl;
}
void other(){
cout<<"in class B, other fun"<<endl;
}
};
int main(){
A * obj = new B();
obj->method();
obj->other();
}
19. 说说类成员的访问权限?
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
- public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
- protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
- private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象访问。
访问权限 | public | protected | private |
---|---|---|---|
本类 | 可见 | 可见 | 可见 |
本类友元 | 可见 | 可见 | 可见 |
子类 | 可见 | 可见 | 不可见 |
外部 | 可见 | 不可见 | 不可见 |
多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
动态多态实现有几个条件:
(1) 一个基类的指针或引用指向派生类的对象
基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
(2)virtual修饰符
🔥21. 虚函数表和虚函数表指针是怎么运作的?如果一个类是局部变量则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。
如果该类是virutal继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。
如果类中成员是virtual属性,会隐藏父类对应的属性。
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中,这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来 *** 作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
通过下面的代码来说明四种不同继承情况下的虚函数表:
#include
using namespace std;
class Base
{
public:
virtual void f() {
cout << "Base: f" << endl;
}
virtual void g() {
cout << "Base: g" << endl;
}
virtual void h() {
cout << "Base: h" << endl;
}
};
typedef void(*Fun)(void);
int main()
{
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表——第一个函数地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun(); // Base:f
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
}
本身的虚函数表为:
- 无虚函数覆盖的一般继承
虚函数按照其声明顺序放于表中;父类的虚函数在子类的虚函数前面。 - 有虚函数覆盖的一般继承
覆盖的 f() 函数被放到了虚表中原来父类虚函数的位置;没有被覆盖的函数依旧。 - 无虚函数覆盖的多重继承
每个父类都有自己的虚表;子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的) - 有虚函数覆盖的多重继承
三个父类虚函数表中的 f() 的位置被替换成了子类的函数指针。
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针?
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
- auto_ptr
#include
using namespace std;
int main(){
auto_ptr<string> p1(new string("我是爱南开的"));
auto_ptr<string> p2;
p2 = p1;
cout<<*p2<<endl;
cout<<*p1<<endl; //报错~野指针
}
采用所有权模式(独占)。p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
- unique_ptr
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。采用所有权模式。
unique_ptr<string> p3(new string("我也是爱南开的"));
unique_ptr<string> p4;
p4 = p3;
上面的p4 = p3;
会报错,直接过不了编译。想要安全的将p4指向p3指向的内容,则需要使用下面的语句:
p4 = move(p3);
- shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。但是当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏.
为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
shared_ptr 是为了解决auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
int main(int argc, char *argv[]) {
int *aa = new int(10)
shared_ptr<int>p1(aa);
cout<<"p1 = "<<*p1.get()<<", p1 count = "<<p1.use_count()<<endl; //输出结果 10,1
shared_ptr<int>p2(p1);
cout<<"p2 = "<<*p2.get()<<", p2 count = "<<p2.use_count()<<endl; //输出结果 10,2 p2是调用复制构造函数,引用计数加1
shared_ptr<int>p3 = p2;
cout<<"p3 = "<<*p3.get()<<", p3 count = "<<p3.use_count()<<endl; //输出结果 10,3 p3是调用赋值函数,引用计数加1
int *val = new int(20);
p1.reset(val); // p1之前存储的是aa指针,调用reset之后会先回收aa指针,然后再存储val指针
cout<<"p1 = "<<*p1.get()<<", p1 count = "<<p1.use_count()<<endl;
cout<<"p1 = "<<*p1<<endl;
shared_ptr<POINTTEST>p(new POINTTEST); // operator->函数是让shared_ptr使用起来跟向存储指针一样,效果相当于存储指针,因为operator->返回的就是存储指针
p->a = 5;
p->b = 9;
cout<<"a = "<<p->a<<", b = "<<p->b<<endl;
return 0;
}
成员函数:
use_count 返回引用计数的个数
unique 返回是否是独占所有权(use_count 为 1)
swap 交换两个shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
- 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。
-
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
-
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
-
智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
-
初始化列表:使用初始化列表来对类进行初始化
-
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
-
atomic原子 *** 作用于多线程资源互斥 *** 作
-
新增STL容器array以及tuple
-
Lambda表达式:
[capture list] (parameter list) -> return type {function body}
。我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。 -
可变参数模板:C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。
对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。处理规则见下
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
1、词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
4、优化:源代码级别的一个优化过程。
5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
将汇编代码转变成机器可以执行的指令(机器码文件)。
汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。
🔥25. C++程序的内存分布是怎么样的?链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
一个程序本质上都是由BSS段、data段、text段三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
BSS段(未初始化数据区):(Block Started by Symbol)通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。
数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配
代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量
可执行程序在运行时又多出两个区域:栈区和堆区。
栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
1、概念:
1)Vector
连续存储的容器,动态数组,在堆上分配空间
底层实现:数组
两倍容量增长:
vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
性能:
访问:O(1)
插入:在最后插入(空间够):很快
在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够):内存拷贝
在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除:很快
在中间删除:内存拷贝
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2)List
动态链表,在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
性能:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:很快,一般是常数开销
删除:很快,一般是常数开销
适用场景:经常插入删除大量数据
2、区别:
1)vector底层实现是数组;list是双向链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
3、应用
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
- 设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言 - 语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
typedef和define可对一个对象进行 取别名(typedef) 或 字符替换(define) ,以此增强程序的可读性。
typedef是关键字,对已经存在的数据类型取别名。在编译阶段处理,会进行类型检查,只能在定义的作用域内使用。
define是预处理指令(宏定义),只进行简单的字符替换,是否产生错误要在编译时才可知。没有作用域限制,可以对类型/变量/常量等进行替换。
29. C++作为面向对象语言,他有哪些特性?封装、继承、多态。
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象 *** 作,对不可信的进行信息隐藏。
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
1、当前头文件目录
2、编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
3、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
1、编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
2、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
- 分配和释放,堆在分配和释放时都要调用函数(MALLOC,FREE),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成空洞),这些都会花费一定的时间,具体可以看看MALLOC和FREE的源代码,他们做了很多额外的工作,而栈却不需要这些。
- 访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。另外,堆的内容被 *** 作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。
综上所述,站在 *** 作系统以上的层面来看,栈的效率比堆高。
32. new/delete与malloc/free的区别?- 首先,new/delete是C++的关键字,而malloc/free是C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数。
- malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存大小,返回的指针不用强转。
- new和delete是 *** 作符,malloc和free是库函数
STL主要由以下几部分组成:
容器、迭代器、仿函数、算法、分配器、配接器
1.容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
2.算法:各种常用的算法,如sort、find、copy、for_each等
3.迭代器:扮演了容器与算法之间的胶合剂
4.仿函数:行为类似函数,可作为算法的某种策略。仿函数是让一个类看起来像一个函数。其实就是一种重载了operator()的class或者class template。
5.适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
6.空间配置器:负责空间的配置与管理
其中仿函数较难理解,这里列出内置的一些仿函数。
1、迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
2、迭代器和指针的区别
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些 *** 作符,->、、++、- -等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,- -等 *** 作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
3、迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。unordered map底层结构是哈希表。由于 map 和set所开放的各种 *** 作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的 *** 作行为,都只是转调RB-tree 的 *** 作行为。
map和set区别在于:
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。map会根据键值自动排序,不允许键值重复,而multimap允许键值重复。
(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
(3)map支持下标 *** 作,set不支持下标 *** 作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)