目录
- 1.const 关键字的使用场景
- 2.const与define
- 3.static 关键字的使用场景
- 4.explict 关键字的使用场景
- 5.volatile 关键字的使用场景
- 6.什么是多态
- 7.虚函数的实现原理
- 8.构造函数可以是虚函数吗
- 9.析构函数可以是虚函数吗,应用场景
- 10.智能指针有哪些,实现原理以及用法
- 11.什么是模板特化
- 12.new 和 malloc 区别
- 13.C++ 内存空间布局
- 14.如何限制对象只能在**堆上**创建
- 15.如何限制对象只能在**栈上**创建
- 16.什么是单例模式,工厂模式
- 17.C++ auto 类型推导的原理
- 18.泛型编程如何实现的
- 19.指针和引用的区别
- 20.动态绑定与静态绑定
- 21.类型转换
- 22.野指针
- 23.泛型编程
- 24.模板
- 25.STL之HashTable
- 26.STL 中 unordered_map 和 map 的区别
- 27.STL之Vector
- 28.内存泄漏
1.const 关键字的使用场景
- 阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化;
- 对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
- 在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值;
- 对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量;
- 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
2.const与define
- define为文本替换,const为常量值。
- const在编译阶段使用,define在预处理阶段展开。
- const有数据类型,有类型安全检查,define无数据类型,无安全检查。
- 可以对const常量进行调试,而宏常量不能进行调试。
- const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。const可避免不必要的内存分配,效率更高。
3.static 关键字的使用场景
- 全局生命周期。用static修饰的函数或变量生命周期是全局的。存储在静态数据区。
- 作用域隐藏。当一个工程有多个文件的时候,用static修饰的函数或变量只能在被本文件可以见,文件外不可见。
- static成员变量,在类内声明,在类外定义和初始化,静态成员变量相当于类域中全局变量,可以被类的所有对象所共享,包括派生类的对象。不定义对象,也可以通过类访问静态成员变量。
- static成员函数,只能调用静态成员函数和静态成员变量,因为静态成员函数没有this指针;静态成员函数不能声明为virtual,const,volatile。
- static修饰的变量默认初始化为0。
4.explict 关键字的使用场景
- 修饰构造函数,防止无预期的类型转换(隐式转换)。
5.volatile 关键字的使用场景
- volatile关键字是一种类型修饰符,被修饰的类型变量表示可以被编译期某些未知的因素改变。
- 用来解决变量在“共享”环境下容易出现读取错误的问题
6.什么是多态
- 静态多态:发生在编译期:函数重载和模板
- 动态多态:发生在运行期:虚函数
- 定义:同一个行为,不同实现。虚函数是实现多态的前提,多态发生在继承中,派生类要对基类的虚函数进行重写。
- 多态用虚函数实现,结合动态绑定。
- 多态必须是通过基类的指针或引用来调用虚函数。
7.虚函数的实现原理
- 虚函数是通过虚函数表来实现的,虚函数表存放了类中所有的的虚函数地址,虚函数表保存在含有虚函数的类的实例化对象的内存空间中,虚函数表的指针存放在对象实例中最前面的位置中。
- 虚函数表是一个存储成员函数指针的数据结构;
- 虚函数表是由编译器自动生成与维护的;
- virtual成员函数会被编译器放入虚函数表中;
- 存在虚函数时,每个对象都有一个指向虚函数表的指针(vptr指针)
- 在实现多态的过程中,父类和派生类都有vptr指针。
8.构造函数可以是虚函数吗
- 不可以。构造函数是实例化对象时调用的,如果将构造函数定义为虚函数,而此时对象并未创建,则不能调用虚函数,故不能定义成虚函数。(虽然虚函数表在编译期就已经存在了,但是虚函数指针存在于对象实例化的内存空间中,故先有了对象才能通过虚函数指针获取虚函数表中的地址进行调用)
- 从类型上看,构造函数实例化对象时需要明确其类型;而虚函数主要是在信息不全的情况下,能使重写的函数得到调用。
- 从使用角度来看,构造函数是实例化对象时自动调用;虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用。
9.析构函数可以是虚函数吗,应用场景
- 可以,为了防止内存泄漏。因为将基类的指针或引用绑定到派生类的对象,如果未将基类析构函数定义为虚函数,当我们调用析构函数时,那么只会调用基类析构函数,释放基类的内存空间,派生类的内存空间不会释放,则会造成内存泄漏。
10.智能指针有哪些,实现原理以及用法
- 智能指针是为了解决动态内存分配时造成的内存泄漏以及多次释放同一块内存。
- 实现原理:将基本类型指针封装成类对象指针,在析构函数中调用delete关键字释放内存空间。
- auto_ptr在c++11已被弃用。
- 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过use_count计算使用者的个数,当个数为0时(即没有使用时),则会自动释放内存空间,避免内存泄漏。
- 独占指针(unique_ptr):资源只能被一个指针占有,故不支持拷贝构造和赋值操作。
- 弱指针(weak_ptr):指向***shared_ptr指向的对象***,能够解决shared_ptr循环引用的问题。循环引用:该被调用的析构函数没有被调用,造成了内存的泄漏
11.什么是模板特化
- 相对于泛型,指定一个特定的类型。类模板可以偏特化和全特化,而函数模板只能全特化。
- 模板偏特化是根据需要,模板的部分参数特化,而不是全部的参数。模板全特化则为全部参数特化。
12.new 和 malloc 区别
- malloc/free是C/C++的标准库函数(头文件支持),new/delete是C++的运算符(编译期支持)。都用于动态内存的申请与释放。
- malloc是一个最底层的函数,不会调用构造函数;new运算符会调用构造函数,函数返回相应类型的指针。
- new运算符有类型安全检查,malloc不检查。
13.C++ 内存空间布局
- 堆区:动态申请的内存空间,由mallo分配的内存块,由程序员分配和释放。
- 栈区:存放函数的局部变量,由编译器分配和释放。效率高,但内存容量有限。
- 自由存储区:和堆十分相似,存放由new分配的内存块,由delete释放。
- 全局区(静态区):存放全局变量和静态变量。
- 常量区:存放的是常量,不允许修改。
- 代码区:存放程序体的二进制代码。比如我们写的函数,都是在代码区的。
- 自由存储区和堆的区别:
- 自由存储是C++中通过new和delete动态分配和释放的抽象概念,而堆(heap)是操作系统和C语言的术语,是操作系统维护的一块动态分配内存。
- new申请的内存区域在C++中成为自由存储区。藉由堆实现的自由存储区(new的底层实现是调用malloc分配内存),可以说new申请的内存区域在堆上
- 自由存储区并不等价于堆,当使用new来分配内存时,可以重载操作符new,改用其他内存实现自由存储。
14.如何限制对象只能在堆上创建
- 类对象只能建立在堆上,就不能静态建立类对象,故不能直接调用构造函数。
- 如果将构造函数设为私有,而new运算符建立对象有两个步骤:1.使用malloc分配内存空间 2.调用构造函数初始化。故不能将构造函数设为私有。
- 当对象在栈上分配空间时,编译器会为对象分配内存空间,调用构造函数。当对象使用完之后,再调用析构函数释放内存。如果析构函数的访问性是private的,编译器无法调用析构函数,则编译器不会在栈上建立对象。
15.如何限制对象只能在栈上创建
- 只有使用new运算符才会在堆上建立对象,故禁用new运算符(new调用operaor new函数申请空间),即把operator new()设为私有。
-
如何让类不能被继承
- 使用关键字final可以使得类不被继承
- 修饰虚函数则不可以被重写
16.什么是单例模式,工厂模式
-
单例模式:创建唯一的一个变量(对象),在类中将构造函数设为protected或者private(析构函数设为相对应的访问权限),故外部不能实例化对象,再提供访问它的一个全局访问点,即定义一个static函数,返回类中唯一构造的一个实例对象。
-
懒汉模式:在系统运行中,实例并不存在,只有当需要的时候才创建并使用实例。(需要考虑线程安全)可以使用静态局部变量(c++11及以上)
-
饿汉模式:指系统一运行,就初始化创建实例,当需要的时候,直接调用就行。(本身就是线程安全)
- 二者的主要区别就是创建实例的时间不同
- 使用懒汉单例时,推荐使用内部静态变量的懒汉单例,代码量少。
- 懒汉式是空间换时间,适应于访问量较少;饿汉式是时间换空间,适应于访问量较大或者线程较多时。
- 线程安全:在拥有共享资源的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
-
-
工厂模式:
- 功能:1.定义创建对象的接口,封装对象的创建;2.具体化的工作推迟到子类中实现。
- 工厂模式的实现方式分为简单工厂模式,工厂方法模式,抽象工厂模式。
- 简单工厂模式:一般应用于多种同类型类的情况,将这些类隐藏起来,再提供统一的接口,便于维护和修改程序中大部分地方;根据类型分支选择不同的产品构造函数。缺点是要求产品的类不多,每增加一个产品类,就要在工厂类增加一个新的分支,违背了开闭原则。
- 工厂类:工厂模式的核心类,会定义一个用于创建指定的具体实例对象的接口。
- 抽象产品类:是具体产品类的继承的父类或实现的接口。
- 具体产品类:工厂类所创建的对象就是此具体产品实例。
- 工厂方法模式:为解决简单工厂模式违背开闭原则而产生。增加对工厂基类的抽象,增加了工厂子类,使得模式更加有弹性。
- 工厂方法模式抽象出了工厂类,提供创建具体产品的接口,交由子类去实现。
- 抽象工厂模式:工厂方法模式适合产品种类结构单一的场合,为一类产品提供创建的接口;而抽象工厂模式适合产品种类结构多的场合。当具有多个抽象产品类型时,抽象工厂便可以使用。
- 简单工厂模式:一般应用于多种同类型类的情况,将这些类隐藏起来,再提供统一的接口,便于维护和修改程序中大部分地方;根据类型分支选择不同的产品构造函数。缺点是要求产品的类不多,每增加一个产品类,就要在工厂类增加一个新的分支,违背了开闭原则。
17.C++ auto 类型推导的原理
-
根据初始值value 来推断类型,auto要求变量必须初始化,即定义变量的时候的必须赋值;
-
auto类型与初始值类型可能会有出入,编译器会适当的改变其结果使auto更符合初始化规则。
auto var = value; const string str01 = "asdad"; for (auto a :str01)//a为char类型 { a = '1';//成功赋值 a不是const类型 }
-
decltype也是用于推导类型的,它根据**(表达式)**来推断类型
decltype (表达式) var = value; //有两个括号,则代表一定是引用类型 decltype ((int)) var = value;//返回int&
18.泛型编程如何实现的
- 泛型即是指在多种数据类型上皆可操作,则泛型编程即是不考虑具体类型的一种编程模式。
- 模板是实现泛型编程的基础。
19.指针和引用的区别
- 指针所指的内存空间在运行中是可变的,而引用已经绑定就不可以改变。(是否可变)
- 指针在内存中占有内存空间的,而引用是别名,不具备内存空间。(是否占内存)
- 指针可以是空指针,而引用必须要绑定对象。(是否为空)
- 指针是多级的,二级,三级指针;而引用就是别名,不存在多级。(是否多级)
20.动态绑定与静态绑定
- 编译期:指编译器将代码与一些资源文件链接编译成可执行文件这一过程。
- 运行期:指运行编译器生成的可执行文件这一过程。
- 对象的静态类型:对象在声明时采用的类型,是在编译期确定的。
- 对象的动态类型:目前所指对象的类型,是在运行期确定的。对象的动态类型可以更改,对象的静态类型不可更改。
- 静态绑定:绑定的是对象的静态类型,发生在编译期。
- 动态绑定:绑定的是对象的动态类型,发生在运行期。只有虚函数才会涉及到动态绑定。
21.类型转换
- static_cast:**用于转换基础类型和具有继承关系的的对象指针。**static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。
- dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。适用于多态类之间的类型转换。
- const_cast:用于将const变量转为非const。
- reinterpret_cast:强制类型转换。适用于不同类型的指针类型转换。
22.野指针
- 野指针是指向不可用内存区域的指针。野指针不是NULL指针,是指向“垃圾“内存的指针。
- 出现的情况:
- 指针在定义的时候未被初始化
- 指针指向动态分配的内存空间在释放(delete/freee)后,未置NULL,让人误以为是合法的指针
- **指针操作超过了变量的作用范围。**例:在函数中将一个局部变量的地址作为函数的返回值,这里编译器会给出警告,因为离开该函数后,局部变量的空间就会释放掉,返回的地址(指针)相当于是野指针。
- 悬挂指针:一个指针既不为空,也没有指向一个已知的对象,就称之为悬挂指针。它指向一块没有分配给用户使用的内存
23.泛型编程
- 定义:独立于任何特定类型方式编写代码
- 模板是泛型编程的基础
24.模板
-
函数模板:建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表;函数模板是一个独立于类型的函数,可作为一种方式,产生函数的特定类型版本。
- 模板函数:使用函数模板之后产生的函数
- 普通函数与函数模板的区别:普通函数可以进行自动类型转换,函数模板必须严格匹配。
- 函数模板也可以被重载。
- 函数模板可以显式使用模板,也可以自动类型推导即隐式使用模板。
- 函数模板通过具体类型产生不同的函数。 函数模板—>模板函数(具体函数)—>被调用。
- 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
-
类模板:可以用来定义一组特定类型的类的类定义
- 使用类模板时,必须为模板形参显式指定实参
- 类模板派生普通类时需要指定类型,要知道对象的类型才能分配内存
-
模板特化:
-
函数模板特化:
关键字 template 后面接一对空的尖括号(<>);
再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;函数形参表;
函数体。template <>//空模板形参 int compare<const char*>(const char* const &v1,const char* const &v2)
-
类模板特化
template<> class className<const char*> { //... }
-
类模板部分特化(偏特化)
如果类模板有一个以上的模板形参,我们也许想要特化某些模板形参而非全部。
部分特化的模板形参表是对应的类模板定义形参表的子集。
template <class T1, class T2> class some_template { // ... }; //偏特化 template<class T1> class some_template<T1, int > { }
-
25.STL之HashTable
- hashtable是采用开链法来完成的(vector + list),hash_table(底层是vector)的每个元素装着list头指针,每个list头指针可以开出一条链表。
- 底层键值(key)序列采用vector实现,vector的大小取的是质数,且相邻质数之间的关系是大约是两倍。
- 对应键的值(Value)序列采用但单向list 实现
- rehashing:当桶内的元素总个数大于桶子个数时(即元素个数超过vector大小时),就会使桶子个数增长到其两倍附近的质数,所以桶子数一定比元素个数多。
26.STL 中 unordered_map 和 map 的区别
- 底层实现不同:unordered_map底层实现是哈希表(元素无序),map的底层实现是RB-Tree(红黑树元素有序)。
- 优缺点:
- unordered_map:查找效率高,通常为常数级O(1),但建立哈希表耗费时间较多
- map:内部元素有序,查找和删除操作都是在O(logn);维护红黑树的结构需要占据一定的内存空间。
- 适用情况:要求查找效率高则使用unordered_map;要求内部元素有序使用map
27.STL之Vector
- vector本质是一个动态数组,底层实现是一段连续的线性内存空间。
- 扩容的本质:
- 当增加新元素时,如果超过当时的容量,会申请一块新的内存空间,一般是原来的两倍,具体情况根据编译器而定;
- 然后将原来的元素逐一拷贝到新的空间,再释放原来的内存空间
- 将旧内存空间的内容释放掉,本质上其存储空间不会释放,只是删除了里面的内容
28.内存泄漏
- 定义:程序中动态分配的堆内存由于某种原因未能释放或无法释放
- 具体情况:
- 释放动态数组时未加[],即应使用delete []
- 两次释放同一内存空间
- 没有将基类的析构函数设置成虚函数;当类指针或引用指向子类对象时,调用析构函数时,则子类的析构函数不会被调用,则子类的资源没有得到释放。