目录
1、C++this指针是干什么用的?
2、C++的new和delete,什么时候用new[ ]申请,可以用delete释放?
3、C++static关键字的作用?
4、C++继承?
5、C++多态?
6、C++空间配置器?
7、vector和list的区别?
8、map与multimap?
9、C++如何防止内存泄漏?
10、C++如何调用C语言语句?
11、C++什么时候会出现访问越界?
12、C++类的初始化列表?
13、C和C++的区别?C和C++的内存分布有什么区别?
14、int* const p 和 const int *p 的区别?
15、malloc 和 new 的区别?
16、map和set?
18、STL、map底层、deque底层、vector里的empty()和size()的区别、函数对象?
19、STL种迭代器失效的问题?
20、struct和class的区别?
21、编译链接全过程?
22、初始化全局变量和未初始化全局变量的区别?
23、构造函数和析构函数可不可以是虚函数?(这个问题都被问烂了,不想多说)
24、构造函数和析构函数能不能抛出异常?
25、宏和内联函数的区别?
26、局部变量存在哪?
27、拷贝构造函数中为什么传引用而不是传值?
28、内联函数和普通函数的区别?
29、如何实现一个可以不被继承的类?
30、什么是纯虚函数?为什么要有纯虚函数?虚函数表存在哪?
31、手写单例模式?
32、说一下C++中const,const和static的区别?
一个类定义了很多对象,对于每个对象它们有自己的成员数据,但是它们共享一套成员方法,那么在成员方法里面是如何区分是哪个对象调用该方法,那就是通过this指针。C++普通的成员方法被编译之后都会多出来一个this指针参数,用来接收调用该方法的对象的地址。
引申以下:
malloc和new的区别?
malloc是按字节开辟内存的;new开辟内存时需要指定类型 new int[10],所以malloc返回值是void*,需要做强转;operator new -> int*,new返回的就是指定类型;
malloc只负责开辟内存,new不仅有malloc的功能,还可以进行数据的初始化;对于类类型,还会在开辟的内存上调用构造函数生成对象;
malloc开辟内存失败返回nullptr指针,可以根据返回值判断内存是否开辟成功;new内存开辟失败抛出bad_alloc类型的异常,所以一般包裹在 try catch 语句块中,来判断是否开辟成功;
free和delete的区别?
delete : 调用析构函数,再free(释放内存);
回归本题:
补充知识点:
局部变量本身不产生符号,处于一个函数的栈帧里面,通过ebp-偏移量来访问。static修饰的局部变量就要产生符号了(存储在数据区),但也是局部的。
首先,继承属于类和类之间的关系,除了继承还有组合关系;
继承有两大好处:
多态:多种多样的状态
引申问题:
是不是虚函数的调用一定就是动态绑定? (肯定不是)
在类的构造函数中,调用虚函数,也是静态绑定(构造函数中调用其它任何函数,都不会发生动态绑定)。也就是在对象生成之前是不能发生动态绑定的,构造函数执行完才能生成对象;其次如果不是通过指针或者引用变量来调用虚函数,也不会发生动态绑定;通过对象本身调用只是静态绑定。
#include
using namespace std;
class Base
{
public:
Base(int data = 0) : ma(data) {}
virtual void show() { cout << "Base::show" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 0) : Base(data), mb(data) {}
void show() { cout << "Derive::show" << endl; }
private:
int mb;
};
int main()
{
Base b;
Derive d;
// 静态绑定 用对象本身调用虚函数,是静态绑定
b.show();
d.show();
// 动态绑定 不管基类指针指向基类对象还是派生类对象,都是动态绑定
// 动态绑定 必须由指针调用虚函数
Base* pb = &b;
pb->show();
Base* pt = &d;
pt->show();
// 动态绑定 必须由引用调用虚函数
Base &p1 = b;
p1.show();
Base& p2 = d;
p2.show();
// 动态绑定(虚函数通过指针或者引用变量调用,才能发生动态绑定)
Derive* pb1 = &d;
pb1->show();
Derive& pb2 = d;
Derive* pd = (Derive*)&b;
pd->show(); // 动态绑定
return 0;
}
空间配置器allocator:是给容器分配内存使用的,主要作用就是把对象的内存开辟和对象构造分开,把对象的析构和内存释放分开;
应用场景:如果我们容器里面存储的是类类型,那么使用传统的new开辟内存的时候,它不仅会开辟内存,并且会在开辟的内存上调用构造函数生成对象,这显示与我们的期望不符合,我们只是想开辟一段内存;当我们想从容器删除一个对象时,我们只是想调用一次析构函数把该对象删除,如果使用delete,那么会直接把容器的整个内存空间释放掉;因此产生了容器的空间配置器;空间配置器主要包含以下四个API接口:
T* allocate(size_t size) // 负责开辟内存
void deallocate(void *p) // 负责内存释放
void construct(T* p,const T &val) // 负责对象构造 在已经开辟好的内存上构造一个值为 val 的对象 调用拷贝构造
void destroy(T *p) // 负责对象析构
vector:底层是一个可2倍扩容的动态数组,提供了尾部的增加 push_back( ) 和删除pop_back( ),时间复杂度都是O(1),内部提供了[ ]运算符的重载,可以通过下标直接访问 O(1),因此适合于随机访问比较多的场合;比如优先级队列底层就是通过vector实现,优先级队列默认是大根堆形式,父子结点通过下标联系在一起,因此内存要连续,使用vector;
list:底层是一个循环的双向链表,每一个结点都是new出来的,不连续;提供了头部的push_front( )和pop_front( ),尾部的push_back( ) 和 pop_back(),时间复杂度都是O(1);适用于增加和删除操作比较多的场合。
vector增加和删除涉及到元素的移动,时间复杂度为O(n),list插入删除为O(1);
map底层是红黑树,里面存储的是一个键值对 [key-value],红黑树也是一颗非严格平衡的二叉搜索树,对元素的key进行排序,然后查找。
区别:map不允许key重复,multimap允许key重复
补充:
红黑树:5个性质 、 插入的3种情况(最多旋转2次)、 删除的4种情况(最多旋转3次)
内存泄漏:分配的堆内存没有释放(因为堆内存没有名字,只能用指针来指向);
解决办法:智能指针,智能指针就是利用栈上的对象出作用域自动析构的特性,在智能指针的析构函数里面进行资源释放,从而帮助程序猿避免内存泄漏。
(智能指针属于类对象)
c++提供了两大类智能指针,带引用计数的和不带引用技术的:
不带引用计数的: auto_ptr、scoped_ptr、unique_ptr(推荐使用)
带引用计数的:shared_ptr、weak_ptr
由于C和C++生成符号的方式不同,所以C和C++语言之间的API接口是无法直接调用的,C语言的函数声明必须扩在extern "C" {......}里面。
曾经有个面试官问我如下代码什么意思?
#ifdef __cpluscplus // C生成的函数接口,可以在C和C++环境下直接使用
extern "C"
{
#endif
int sum(int,int);
#ifdef __cpluscplus
}
#endif
如果是C++编译环境,则extern“C”展开,告诉编译器函数sum要按照C风格进行编译;
对于sum函数,C++编译器产生的符号为 sum_int_int,C编译器产生的为 sum。(这也是C++可以实现函数重载的原因)
可以指定对象成员变量的初始化方式,尤其是指定成员对象的构造方式;
内存分布:实际上没有什么区别,都是划分为用户区和内核区
user space: reserve .text .rodata .data .bss heap stack 命令行参数和环境变量
kernal space: ZONE_DMA 、ZONE_NORMAL(.text .rodata .data .bss heap stack) 、ZONE_HIGHMEN
int* const p: const修饰的是指针p本身,也就是说指针的指向不能再修改,但是指向的内容可以修改;
const int* p: const修饰的是*p,也就是p指向的内容,所以p的指向可以变,但是指向的内容不能再修改;
set集合,只存储key; map映射表,存储[key-value]键值对,底层数据结构都是红黑树;
是在堆上分配的。
STL提供了以下标准容器:
顺序容器:vector、list、deque
关联式容器(有序、无序):map、set、multimap、multiset、unordered_map、unordered_set
容器适配器:stack、queue、priority_queue
迭代器:透明的访问容器的每一个元素;
泛型算法:接收的参数都是迭代器;
map:底层红黑树;
deque:底层是动态可扩容的二维数组,一维默认为2,二维大小固定4096/数据类型,扩容时一维进行扩容;
vector底层有三个指针成员,first、last、end;empty是比较 first =last ,返回bool值;size是用 last - first,返回元素个数;
函数对象(仿函数):有()小括号运算符重载的对象,看起来就像函数一样,但其实是对象调用了它的小括号运算符重载函数,函数对象一般使用在泛型算法与容器当中,比如sort、find、priority_queue、set、map等等。
迭代器是不允许一边读一边修改的,当通过迭代器插入一个元素,若引起扩容,所有的迭代器就都失效了,若没扩容,则插入位置之后的都失效了;当通过迭代器删除一个元素,当前删除位置后面所有元素的迭代器就都失效了;
解决办法:当通过迭代器更新容器元素以后,要及时对迭代器进行更新。insert/erase方法都会返回新位置的迭代器。
1、定义类的时候区别:struct默认公有,class默认私有
2、继承时:class B :A(私有继承) struct B :A(公有继承)
3、struct空结构体是0,struct空类是1
预处理、编译、汇编(生成二进制可重定位obj文件)*.o
链接:1、合并段,符号解析;2、符号的重定向;3、生成可执行文件
.data(初始化且初始值不为0) .bss(未初始化或初始化为0)
构造函数不可以:
析构函数可以:若非虚析构,则静态绑定,造成内存泄漏;
构造函数不能抛出异常:假如在执行构造函数时候抛出异常,说明对象构造失败,就调用不了对象的析构函数,那么构造函数中抛出异常以前所创建的资源就无法进行释放,造成内存泄漏;
同理析构函数也不能抛出异常,也涉及到资源释放的问题。
尽量做到资源的自动释放-------智能指针
宏:#define,预编译阶段,纯粹就是字符串替换。
内联:inline,编译阶段,在函数的调用点,根据函数的实参直接将代码进行展开,就节省了函数调用的开销。
调试:宏没有办法进行调试,所以如果用宏定义一个函数,出问题了没办法进行调试;内联函数inline调试的时候是在debug版本下,和普通函数一样,有函数调用的过程,就可以进行调试了;
#define 可以定义 常量、代码块、函数
inline 只能修饰函数
存储在栈上:stack,栈上的变量不产生符号,符号表中是没有的,通过ebp指针-偏移量来进行访问。
比如:int a = 10 => mov dward ptr[epb-4],0Ah
注意:必须传引用!!!!!!!! 传引用
因为我们调用拷贝构造函数本身就是想用一个已经存在的对象来构造另一个对象,那么如果传值的话,就要先产生拷贝构造函数的形参对象,那么这个过程还是要调用拷贝构造,就是要先调用自己产生形参,那么这个过程是矛盾的,直接编译就过不去,编译错误!!!
所以一定记得传引用!!!
核心:函数调用的开销,内联函数没有函数调用的开销,普通函数有函数调用的开销。
函数调用的过程:
释放栈:
基类的构造函数私有化,因为派生类在构造的时候首先要调用基类的构造函数初始化从基类继承下来的成员变量,那么基类构造函数私有化之后,派生类就调用不了,也就无法构造了。
1、形如:virtual void fun() = 0; 就是纯虚函数,含有纯虚函数的类为抽象类,不能实例化产生对象;
2、一般定义在基类里面,基类不代表任何的实体,它的主要作用之一就是给所有的派生类提供一个统一的可以重写接口,这样就实现了多态。因为基类不需要实例化,所以基类的方法也就不知道怎么去实现。
3、虚函数表是在编译期间产生的,运行的时候加载到 .rodata段,只读数据段。
#include
#include
using namespace std;
/*
1.构造函数私有化
2.删除拷贝构造和赋值重载
3.在本类中定义一个私有的对象或对象指针
4.提供一个公有接口,得到该实例对象
由于调用接口有两种方式:
1.构造一个对象,通过对象调用
2.通过类名加作用域来访问接口(接口必须是静态的)
但由于构造函数已私有化,所以第一种方式排除,只能通过第二种,但是通过类名加作用域访问
的成员方法必须是静态的,所有要把接口设计为静态的,最后因为静态的成员方法只能访问静态的
成员数据,所以要提供一个静态的实例对象或者对象指针
非静态的成员方法可以访问所有的成员数据
静态的成员方法只能访问静态的成员数据
*/
#if 0
//饿汉式
//不用考虑线程安全问题,在程序启动的时候就已经调用构造函数开辟空间实例化对象了
class Singleton
{
public:
/*3.获取类的唯一实例对象的接口方法*/
static Singleton* getInstance() //静态的成员方法,访问私有的静态成员数据
{
return &instance;
}
private:
/*2.定义一个唯一的类的实例对象*/
static Singleton instance; //私有的静态成员数据,只能类内访问,类外无法引用
/*1.构造函数私有化*/
Singleton()
{
}
//去掉拷贝构造和赋值重载
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Singleton Singleton::instance; // 静态变量,类内声明,类外定义
#endif
#if 0
std::mutex mtx;
//懒汉式 线程安全方式1
class Singleton
{
public:
/*3.获取类的唯一实例对象的接口方法*/
// 是不是可重入函数呢?不是 锁+双重判断---->线程安全的懒汉式单例模式
static Singleton* getInstance() //静态的成员方法,访问私有的静态成员数据
{
//lock_guard guard(mtx); //锁的粒度太大了
if(instance == nullptr)
{
lock_guard guard(mtx); //锁放这,减小粒度
if(instance == nullptr)
{
/* 非原子操作
开辟内存
构造函数
给instance赋值
*/
instance = new Singleton();
}
}
return instance;
}
private:
/*2.定义一个唯一的类的实例对象指针*/
static Singleton *volatile instance; //私有的静态成员数据,只能类内访问,类外无法引用
/*1.构造函数私有化*/
Singleton()
{
}
//去掉拷贝构造和赋值重载
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Singleton *volatile Singleton::instance = nullptr; // 静态变量,类内声明,类外定义
#endif
//懒汉式 线程安全方式2
class Singleton
{
public:
//线程安全精简的懒汉单例模式
static Singleton* getInstance() //静态的成员方法,访问私有的静态成员数据
{
static Singleton instance; //函数内部的一个静态局部变量,程序启动阶段内存就分配好了,在数据段
//静态对象的初始化是在程序第一次运行到它的时候才初始化
//函数静态局部变量的初始化,在汇编指令上已经自动添加互斥指令了
//不用担心线程安全的问题
return &instance;
}
private:
/*1.构造函数私有化*/
Singleton()
{
//很多初始化的代码
}
//去掉拷贝构造和赋值重载
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main()
{
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
Singleton *p3 = Singleton::getInstance();
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
return 0;
}
C++中的const定义的常量,它的编译方式比较特殊:编译过程中,所有出现常量名字的地方,用常量的值进行替换。
const int a = 10;
int *p = (int*)&a;
*p = 20;
cout << a << *p << endl; // 输出 10 20
最后相当于:cout << 10 << *p << endl; // a被10替换了
C++里面const还可以定义常成员方法:Test *this => const Test *this,这样普通对象和常对象就都可以调用了,当然只能对成员数据进行读操作,不能进行写操作。
const和static的区别:
面向过程:
const能修饰全局变量、局部变量、形参变量,const 不能修饰函数
static: 全局变量、形参变量,可以修饰函数,本文件可见
面向对象:
const :常方法/成员变量 Test *this => const Test *this ,依赖对象,通过对象调用
static :静态方法/成员变量 Test *this => delete(没有this指针) ,不依赖对象,通过类名加作用域调用
33、四种强制类型