顶层const和底层const
变量自身不能改变的是顶层const,比如const int,int *const的常量指针,变量所指的对象或者所引用的对象是不能改变的,而变量自身是可以改变的是底层const,比如const int *的指向常量对象的非常量指针。
左值和右值
左值是有具体存储地址的值,表现为=左边的值,右值是没有具体存储地址,比如寄存器中的值,表现为=右边的值。名字的左值:该名字代表的存储单元的地址;名字的右值:该名字代表的存贮单元的内容。
智能指针
- // 初始化方式1
- std::unique_ptr<int> up1(new int(1));
- std::unique_ptr<int[]> up2(new int[3]);
- // 初始化方式2
- std::unique_ptr<int> up3;
- up3.reset(new int(1));
- std::unique_ptr<int[]> up4;
- up4.reset(new int[3]);
- // 初始化方式3,推荐
- std::unique_ptr<int> up5 = std::make_unique<int>(1);
- std::unique_ptr<int[]> up6(std::make_unique<int[]>(3));
- /* 没有尝试过std::unique_ptr<int> up(std::make_unique<int>(1));
- * 和std::unique_ptr<int[]> up = std::make_unique<int[]>(3);是否正确
复制代码 这样获得的up内就包含了指向创建的内存的指针,可以用up.get()来获取该指针,和直接使用up是等价的。
shared_ptr和weak_ptr见cubox收藏,auto_ptr在C++11已经弃用。
模板
函数模板:- // 定义
- template <typename T>
- inline T const& Max (T const& a, T const& b) {
- return a < b ? b : a;
- }
- // 使用
- int i = 1, j = 2;
- cout << Max(i, j);
复制代码 其中carpture是捕获列表,params是参数,opt是选项,ret则是返回值的类型,body则是函数的具体实现。
- 捕获列表描述了lambda表达式可以访问上下文中的哪些变量:
[]:表示不捕获任何变量。
[=]:表示按值捕获变量,也就是说在lambda函数内使用lambda之外的变量时,使用的是拷贝。
[&]:表示按引用捕获变量,也就是说在lambda函数内使用lambda之外的变量时,使用的是引用。
[this]:值传递捕获当前的this。
- params表示lambda的参数,用在{}中。
- opt表示lambda的选项,例如mutable。
- ret表示lambda的返回类型,也可以显示指明返回类型,lambda会自动推断返回类型,但是值得注意的是只有当lambda的表达式仅有一条return语句时,自动推断才是有效的。
静态变量
全局(静态)存储区:分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:****全局或静态对象当且仅当对象首次用到时才进行构造。
回调
自己的函数调用了别人的函数,其中别人的函数又调用了自己的函数,就是回调;回调是函数指针的应用场景。
比如自己调用sort函数,使用自己定义的cmp比较函数,这就是回调,因为sort调用了自己的cmp比较函数,并且是通过函数指针的形式调用的(sort在实现时寻找了cmp函数的入口地址)。
nullptr调用成员函数可以吗?为什么?
能,因为在编译时对象就绑定了函数地址,和指针空不空没关系。
说说什么是野指针,怎么产生的,如何避免?
- 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
- 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
- 避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
说说内联函数和宏函数的区别?
- 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
- 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
- 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等。
★说说new和malloc的区别,各自底层实现原理
- new是操作符,而malloc是函数。
- new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
- malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
- new可以被重载;malloc不行。
- new分配内存更直接和安全。
- new发生错误抛出异常,malloc返回null。
答案解析
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:
- 创建一个新的对象
- 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
★程序启动的过程
- 操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
- 加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
- 加载器针对该程序的每一个动态链接库调用LoadLibrary 。
(1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。
(2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。
(3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3
(4)调用该动态链接库的初始化函数。
- 初始化应用程序的全局变量,对于全局对象自动调用构造函数。
请简述一下atomoic内存顺序(网上搜不到)
有六个内存顺序选项可应用于对原子类型的操作:
- memory_order_relaxed:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。
- memory_order_consume:memory_order_consume只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。
- memory_order_acquire:使用memory_order_acquire的原子操作,当前线程的读写操作都不能重排到此操作之前。
- memory_order_release:使用memory_order_release的原子操作,当前线程的读写操作都不能重排到此操作之后。
- memory_order_acq_rel:memory_order_acq_rel在此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。
- memory_order_seq_cst:memory_order_seq_cst比std::memory_order_acq_rel更为严格。memory_order_seq_cst不仅是一个"获取释放"内存顺序,它还会对所有拥有此标签的内存操作建立一个单独全序。
除非你为特定的操作指定一个顺序选项,否则内存顺序选项对于所有原子类型默认都是memory_order_seq_cst。
构造函数分类
默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数- // 定义
- template <class T>
- class Stack {
- private:
- vector<T> elems;
- public:
- void push(T const&);
- void pop();
- T top() const;
- bool empty() const{
- return elems.empty();
- }
- };
- template <class T>
- void Stack<T>::push (T const& elem) {
- elems.push_back(elem);
- }
- template <class T>
- void Stack<T>::pop () {
- if (elems.empty()) {
- throw out_of_range("Stack<>::pop(): empty stack");
- }
- elems.pop_back();
- }
- template <class T>
- T Stack<T>::top () const
- {
- if (elems.empty()) {
- throw out_of_range("Stack<>::top(): empty stack");
- }
- return elems.back();
- }
- // 使用
- Stack<int> intStack;
- Stack<string> stringStack;
复制代码 说说一个类,默认会生成哪些函数
- 无参的构造函数
- 拷贝构造函数
- 赋值运算符
- std::generate(v.begin(), v.end(), std::rand); //使用随机数填充vector向量v
复制代码 - 析构函数(非虚)
★说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
参考答案
- 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
- 如果类里面有成员类,成员类的构造函数优先被调用(也优先于该类本身的构造函数);
- 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
- 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
- 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现(基类设计者的责任是提供一组适当的基类构造函数)。
- 综上可以得出,初始化顺序:
父类构造函数–>成员类对象构造函数–>自身构造函数
其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
析构顺序和构造顺序相反。
简述下向上转型和向下转型
- 子类转换为父类:向上转型,使用dynamic_cast(expression),这种转换相对来说比较安全不会有数据的丢失;
- 父类转换为子类:向下转型,可以使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。
★模板的实例化和具体化
[code]// #1 模板定义templatestruct TemplateStruct{ TemplateStruct() { cout |