C++基础知识面试必备、复习细节 (1)
c++变量与基本类型
(解决面试时常问的考点以及易忘点易混点)
一些经验准则:
- 如果明确数值不为负,则选择无符号类型
- 使用int执行整数运算(如果超出int数值范围则采用 long long)
- 执行浮点数运算采用double(注意double类型不可以用==判断相等,可以用相差值小于一个很小的值判断)
- 类型转换时要注意范围,浮点转换到整型会损失小数部分
- 切勿混用带符号和无符号类型。(当无符号和有符号相加时,带符号数会转变为无符号数,会引发错误)
- 定义变量时尽量初始化,尤其在函数体内,因为在函数体内的内置类型的变量如果没有初始化其值是未定义
- 引用和指针
- 引用是对象的一个别名,指针本身就是一个对象
- 引用必须在定义时进行初始化,而指针无需(但为了避免未知错误,通常定义指针时赋初值)
- 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了
- ”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
- 将引用作为函数的参数时,实际上就是直接将该变量传入了函数,而不是再次拷贝了一个临时变量,所以对该参数的修改将直接修改的原始的值。
const限定符
为了避免对值的修改,可以采用const限定符。注意:const对象必须初始化
const int a; //错,必须初始化
const int b=10; //对,编译时初始化
const 引用:对常量的引用,对常量的引用不能被用作修改它绑定的对象
const int ci=1024;
const int &ri=ci; //正确,引用以及其对应的对象都是常量
int &r2=ci; //错误,试图让一个非常量引用指向一个常量对象
const指针
指向常量的指针: const int * 表示的是一个指向const int的指针
const int a=3;
const int *p=&a; //正确,a是一个int常量,p是指向int常量的指针
常量指针: int *const p 表示的是指向int的指针,但其不能被修改,不变的是指针本身而不是那个值
故 const int * const p就是一个指向const int的常量指针
int a=0;
int *const p=&a; //p是一直指向a的指针
const int b=0;
const int *const p2=&b; //p2是指向常量对象的常量指针
引用和指针的区别:
- 在任何情况下都不能使用指向空值的引用,使用引用的代码效率比使用指针要高
- 在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空
- 指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向初始化时被指定的对象,以后不能改变
处理类型
- 采用关键字 typedef 创建易懂的变量别名
- typedef double wages; //wages将是double的同义词
- wages a; //a是double类型
auto类型,可以让编译器自动分析表达式类型
auto item=val1+val2; //如果val1和val2是int,则item将是int
vector<int> array;
for(auto i:array) //自动遍历array中的每一个元素{ }
自定义struct 结构体
struct Sales_date
{
string book_name;
unsigned int prices=0;
}; //注意加分号
标准库类型string 可变长的字符串
初始化
string s1; //默认初始化
string s2="hello"; //s2初始化为"hellp"
string s3(10,'a'); //s3的内容是"aaaaaaaaaa"
一些常用操作:
s.empty(); //s为空则返回true,否则返回false
s.size(); //返回字符串长度 s.length()
s1+s2 ; //返回s1和s2连接后的字符串
s1 < > <= >= s2 //返回的是字典序比较,逐一比较的结果
find (string s, size_t pos)//在当前字符串的pos索引位置开始,查找子串s,返回找到的位置索引
next_permutation(s1.begin(),s1.end()); //下一个字典序,在求全排列时常用到
标准库类型vector
vector是一个对象的集合,其中所有对象的类型都相同。容器
初始化
vector<T> v1;
vector<int> a{1,2,3,4,5};
vector<int> array(10); //表示array长度为10,都初始化为0
vector<int> d(10,1); //初始化为10个1
int array[]={1,2,3,4,5,6,7};
vector<int> ve_array(begin(array),end(array)); //用数组初始化vector
常用操作
//添加元素 push_back()
v1.push_back(5); //向尾部加入5
//删除元素 pop_back()
v1.pop_back()
//判断是否为空
v1.empty()
//vector的长度
v1.size()
//遍历vector
for(auto i:v1)
something();
for(auto i=v1.begin();i<v1.end();i++)
something();
for(int i=0;i<v1.size();i++)
something();
//查找某一元素 v1.find()
v1.find() //如果找到则返回对应下标,否则返回的是v1.end()
数组名和指针的区别与联系
数组名存的就是数组第一个元素的地址,即 array==&array[0];
同样,可以令指针指向该数组,则指针所存内容也是数组第一个元素的地址,即可以将数组名赋值给指针
数组名是不可以修改的,而指针是可以移动的,可以用指针遍历一个数组
array+1表示array[1]的地址
sizeof(array)=sizeof(T)*length //sizeof一个数组返回的是整个数组占用的空间,而sizeof指针返回的是一个指针的空间
C++表达式
逻辑运算符求值的短路求值
- 对于逻辑与运算符,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 也就是说,如果左侧为假,则直接返回false而不继续判断右侧。
index<array.size() && array[index]>0 //通过该方式避免越界
- 对于逻辑或运算符,当且仅当左侧运算符为假的时才对右侧对象求值。
- 也就是说,如果左侧为真,则表达式为true已经确定直接返回,而不继续判断
if(s.empty() || s[s.size()-1]=='.') //s为空或者以句号结尾则换行
cout<<endl;
递增递减运算符
前置和后置的递增运算符:
- 前置版本:首先将运算对象加1,然后将改变后的对象返回
- 后置版本:将运算对象加1,返回运算对象改变前的值的副本
int i=0,j;
j=++i; //j=1,i=1
j=i++; //j=1,i=2
通常尽量使用前置版本,如果为了赋值然后遍历,则通常采用后置版本
array[i++]=k; //令array[i]=k且向后遍历一步
位运算符
按位与 & ,将参与运算的两个分量对应的每一位来做逻辑与运算,若两者都为真(等于1),则结果才为真(等于1)。否则为假(等于0 )
即:1 & 1 = 1 、1&0 = 0 、0&1 = 1、0&0 = 0
按位或 | , 将参与运算的每个分量对应的每一位来做逻辑或运算,即两者都为假(为0)时,才为假(为0),否则为真。
即:0|0 = 0、1|0 = 1、0|1 = 1、1|1 = 1
按位异或 ^ , 把参与运算的每个分量对应的每一位来做异或运算,即两者相同为假,不同为真
即:0|0 = 0、 1|0 = 1、0|1 = 1、 1|1 = 0
按位取反 ~ , 把二进制位的每一位进行取反运算,将二进制中1变成0,0变成1
按位右移 >> 把二进制位整体向右移动。
7>>1 即: 0000 0111 >> 1 结果: 0000 0011 = 3
右移等于除了2的N次方,N为右移的位数。
按位左移 << 把二进制位整体向左移动。
sizeof运算符
- sizeof运算符返回一条表达式或一个类型名字所占的字节数。
- 对类型为引用的对象执行sizeof运算将得到被引用对象所占空间的大小
- 对类型为指针的对象执行sizeof运算将得到指针本身所占空间的大小
- 对数组执行sizeof运算将得到整个数组所占空间大小,等价于对数组每个元素sizeof然后求和对string和vector类执行sizeof运算只会返回该类型固定部分的大小而不是占用空间的大小结构体、union、类为空时,对其sizeof返回为1
- 静态变量在sizeof计算时不需要考虑,对结构体进行sizeof不是单纯的加法,要注意对齐效果,即每个变量起始必须是自己所占字节的倍数。结构体的大小必须是其中最大宽度的类型大小的整数倍,
例子:
struct test
{
char a; //1
int c; //4
};
cout<<sizeof(test); //是8而不是5,因为int跳过char后面3个空间再放置,字节对齐
struct test
{
char a; //1
int *p; //8
char b; //1
};
cout<<sizeof(test); //是24而不是10,首先p需要对齐所以需要到8的倍数处才可以放置,然后放置b后,结构体的大小需要是p的整数倍,故最终为24
struct test
{
};
cout<<sizeof(test); //是1而不是0,哪怕是空结构体也占1字节
对union对象进行sizeof时,结果为其中最宽的数据类型的长度
union test
{
char a;
int p;
char b;
};
cout<<sizeof(test); //是4,最大的是int,4字节
对类对象进行sizeof时,大体和struct类似,但要注意虚函数、继承等关系,虚函数需要有指针指向虚函数表故会增加一个指针的空间,继承时会继承父类的空间
class test
{
~test() { }
}; //sizeof(test)=1
class test2
{
virtual ~test2() {}
}; //sizeof(test2)=8 因为是虚函数,所以需要创建一个指针指向虚函数表,占据内存
sizeof(test2)=8 因为是虚函数,所以需要创建一个指针指向虚函数表,占据内存
局部变量和全局变量
- 作用域:全局变量在函数外声明,作用域是整个源程序。
- 局部变量在函数内或循环内声明或作为函数形参,作用域是当前函数或者循环等。
- 内存存储:全局变量存储在全局数据区,局部变量存储在栈区
- 生命期:全局变量存在于整个执行过程,在程序启动时创建,直至程序结束才销毁。
- 局部变量随着函数或者循环的结束就销毁了。
- 如果在函数内部定义了与全局变量相同的局部变量,则在该函数内部优先使用局部变量
- 局部静态对象:static 局部静态变量在程序的执行路径第一次经过时初始化,直至程序终止才被销毁
static关键字
1.全局静态变量 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
2. 局部静态变量 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
3.静态函数 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突; warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
4.类的静态成员 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
5.类的静态函数 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:
<类名>::<静态成员函数名>(<参数表>);
一个面试题
有极大量的数据,10亿个int数据,如何去重?
思路:可以建立hash映射,只需要一个bit存储即可,如果一个数字已经出现则之后遇到就去重,这样至多需要MAX_INT个bit存储,内存是可存的。
或者使用布隆过滤器
https://www.cnblogs.com/btdxqz/p/6895068.html
参数传递的一些细节
- 如果形参是引用类型,则将绑定到对应的实参;否则将会将实参的值拷贝后赋给形参
- 传值参数的修改将不影响具体的实参值,而传引用参数的修改将直接修改具体的实参;
- 通过传指针形参,也可以间接地修改对象的值
- 如果参数是比较大的类对象或者容器对象,通常传引用可以避免低效地拷贝(甚至一些类型不支持拷贝)
- 如果函数无须改变形参的值,则最好将其声明为传引用,且常量引用
- 数组作为形参时,实际上传递的是指向数组首元素的指针。以数组作为形参时要确保不要越界
- 不要返回局部变量的引用或指针! 因为在函数完成后其存储空间将被释放掉,会导致访问的内存非法
- 可以在形参列表中赋值设置默认实参,设置默认实参的参数应该放在右边
重载overload与重写override
重载:同一作用域内函数名字相同,形参列表不同。不允许两个函数除了返回类型外其他所有要素都相同
不能根据返回值判断是否重载;重载后,编译器会根据参数选择最优的调用,要避免二义性
重写:子类重新定义父类中有相同名称和参数的虚函数(virtual)。在继承关系之间。C++利用虚函数实现多态
重写函数必须具有相同的类型、名称、参数列表
内联函数 inline
将函数声明为内联函数将提高函数的执行效率,把关键词inline放在函数定义前面即可。
内联函数执行时会将它在每个调用位置展开,避免了函数调用的开销
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用
不用盲目使用内联,通常函数较为简短且简单时才使用内联
C++基础知识面试必备、复习细节 (2)
类基础知识
c语言强调面向过程,c++强调面向对象
- 定义public说明符之后的成员在整个程序内可被访问;
- 定义private说明符之后的成员只可以被类的成员函数访问,即隐藏了实现细节;
- 定义protected说明符则说明该类或该类的子类、友元可以访问
使用struct和class定义类的唯一区别在于默认的访问权限不一样,
- struct的默认访问权限为public,
- class的默认访问权限为private
封装的优点:确保用户代码不会无意破坏封装对象的状态,且被封装类的具体实现可以在不影响用户代码的前提下完成修改
sizeof一个空类的结果是1,如果有虚函数则会增加一个虚函数指针的大小,静态类数据成员不影响类大小,类成员的大小只受其变量的影响,函数存储在公共代码区
类中成员函数使用名字顺序:优先在成员函数中查找该名字声明,如果没有找到再去类中查找,因此如果形参的变量名和类的变量名相同则默认为形参的,访问类的变量可以用this指针访问
this指针:指向当前类对象,是一个 * const
继承是"is a"关系,组合是"has a"关系
构造函数和析构函数
- 构造函数为类分配内存和初始化,析构函数则是释放内存
- 构造函数和析构函数不返回任何类型
- 构造函数可以被多次重载,参数列表不同即可
- 构造函数可以通过在函数参数列表后加上初始化器更方便地进行初始化
- 默认的拷贝构造函数只是进行成员变量的简单赋值(浅拷贝),因此如果有内存分配则需要自己重新定义拷贝构造函数
- 如果有自己进行内存分配,则需要自己写析构函数
- 构造时从上到下:类先调用父类的构造函数再调用子类的构造函数;
- 析构时从下到上:类先调用子类的析构函数再调用父类的析构函数;
- 如果没有显式定义构造函数和析构函数,系统会隐式构造默认构造函数。只有类没有声明任何构造函数时系统才会生成默认构造函数
派生类构造函数名(总参数列表) : 父类构造函数名(参数列表) //如果不调用父类构造函数,则使用默认构造函数
拷贝控制
拷贝构造函数的参数是自身对象类型的引用(必须是引用,否则岂不是永远陷入了拷贝的无限循环)
可以通过重载赋值运算符’='实现赋值拷贝
通常,拷贝函数的调用时机为:函数的参数为类的对象或函数的返回值是类的对象时
拷贝分为浅拷贝和深拷贝:
浅拷贝:按字节拷贝。这在一些情况会产生问题:如果要拷贝的类中有指针变量或数组之类的,则按字节拷贝会直接按字节进行拷贝,因此就会出现两个指针指向同一内存的情况,当一个类析构时会释放内存,则另一个指针将指向一个未知区域,造成野指针,数组也是同理,当一个类析构时会释放内存,则造成了内存泄漏,因此如果有指针或者数组之类变量,需要采用深拷贝。
深拷贝:每个对象共同拥有自己的资源,必须显式提供拷贝构造函数和赋值运算符。
深拷贝在拷贝数组或指针时将重新开辟新的区域进行存储然后拷贝值
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
如果要对指针变量进行浅拷贝,可以采用 std::shared_ptr 解决该问题
友元
友元函数:在类中,用在外部函数申明前加关键字friend,可以允许这个外部访问本类protected 和 private 的成员。友元函数不是类成员函数,是类外的函数,但是可以访问所有成员
class Point{
public:
friend void fun(Point t);//友元函数
private:
int a;
protected:
int b;
};
void fun(Point t)
{
t.a = 100;
t.b = 2000;
cout << t.a << " " << t.b << endl;
}
友元类:可以定义一个class是另一个的friend,以便允许第二个class访问第一个class的 protected 和 private 成;即如果类A是类B的友元类,则A就可以访问B的所有成员(成员函数,数据成员)
class Point{
friend class Line; //友元类声明方式
private:
int x;
};
友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类
继承与多态
面向对象编程的三个基本概念:数据抽象(类)、继承、动态绑定(多态)
派生类从基类中继承数据和函数,派生类可以从基类中继承public和protected对象,基类的private只有基类本身和友元可以访问。protected对外等价于private,对子类等价于public
定义方式:在类后写类派生列表指明从哪个籍类继承
class Bulk_quota:public Quota
{
public:
xxx
private:
xxx
}
继承分为公有继承、私有继承和受保护继承
公有继承:则外部也可以访问父类的public函数和变量:public成员保持不变,private成员不可见,protected成员也保持不变
私有继承:除了自己和友元,其他都不能访问父类的函数和变量:继承的public和protected变成了private
保护继承:自己、友元、子类可以访问父类的函数和变量,外部不可访问:原先的public变成了protected,private保持不变
基类通常都要定义一个虚析构函数,即使该函数不执行任何操作
因为派生类都包含有基类的部分,所以可以将派生类的对象当成基类对象来使用,基类的指针或引用可以绑定到派生类对象。因此基类的指针指向的对象可能与其动态类型不一致。不可以将基类转换为派生类
Quota item; //基类
Bulk_quota bulk; //派生类
Quota *p=&item; //将基类指针指向基类对象
p=&bulk; //基类指针指向派生类对象
Quota &r=bulk; //基类引用绑定派生类对象
每个类控制自己的初始化过程,首先初始化基类的部分,然后再按照顺序依次初始化派生类成员
不要做重复工作:基类的变量通常调用基类的构造函数初始化,不要在派生类中直接初始化
Bulk_quota(cosnt string& book,double p,double disc):
Quota(book,p),discount(disc){};
可以在类名后加一个关键字final防止继承
友元关系不可以被继承
说说c++的多态特性:
多态:静态多态(编译时期确定行为,包括函数重载、模板、运算符重载等) 动态多态(运行时期才可以确定行为,如虚函数)
拥有Virtual 关键字的函数称之为虚函数,虚函数的作用是实现动态绑定的,也就是说程序在运行的时候动态的的选择合适的成员函数。
要成为虚函数必须满足两点:
- 一就是这个函数依赖于对象调用,因为虚函数就是依赖于对象调用,因为虚函数是存在于虚函数表中,有一个虚函数指针指向这个虚表,所以要调用虚函数,必须通过虚函数指针,而虚函数指针是存在于对象中的。
- 二就是这个函数必须可以取地址,因为我们的虚函数表中存放的是虚函数函数入口地址,如果函数不能寻址,就不能成为虚函数。
虚函数:在函数声明前加一个virtual,虚函数是允许被其子类重新定义的成员函数
构造函数不可以声明为虚函数,因为还没过完成构造此时还没有虚函数表
基类的析构函数通常写成虚函数:防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。
虚函数表(vtable):每个类都拥有一个虚函数表,虚函数表中罗列了该类中所有虚函数的地址,排列顺序按声明顺序排列
虚表指针(vptr):每个类有一个虚表指针,当利用一个基类的指针绑定基类或者派生类对象时,程序运行时调用某个虚函数成员,会根据对象的类型去初始化虚指针,从而虚表指针会从正确的虚函数表中寻找对应的函数进行动态绑定,因此可以达到从基类指针调用派生类成员的效果。
存在虚函数的类至少有一个(多继承会有多个)一维的虚函数表叫做虚表(virtual table),虚表属于类成员,虚表的元素值是虚函数的入口地址,在编译时就已经为其在数据端分配了空间。编译器另外还为每个类的对象提供一个虚表指针(vptr),指向虚表入口地址,属于对象成员。在实例化派生类对象时,先实例化基类,将基类的虚表入口地址赋值给基类的虚表指针,当基类构造函数执行完时,再将派生类的虚表入口地址赋值给基类的虚表指针(派生类和基类此时共享一个虚表指针,并没有各自都生成一个),在执行父类的构造函数。
C++多态的实现过程,可以得出结论:
有虚函数的类必存在一个虚表。
虚表的构建:基类的虚表构建,先填上虚析构函数的入口地址,之后所有虚函数的入口地址按在类中声明顺序填入虚表;
派生类的虚表构建,先将基类的虚表内容复制到派生类虚表中,如果派生类覆盖了基类的虚函数,则虚表中对应的虚函数入口地址也会被覆盖,为了后面寻址的一致性。
纯虚函数和抽象基类:
class CShape
{
public:
virtual void Show()=0; //用等于0表示纯虚,不对函数进行定义
};
声明纯虚函数后,则该类就变成了抽象基类,抽象基类不可以被实例化,只能被继承。
运算符重载
通常情况下,不要重载逗号、取地址、逻辑与逻辑或
可以用友元函数方式重载,也可以类函数方式重载
重载输入输出时,通常返回 istream 和 ostream的引用,这样有左值作用,可以连续输入输出
//重载输出运算符 <<
ostream &operator<<(ostream &os,const Sale_data &item)
{
os<<otem.units_sold<<" "<<item.revenue;
return os;
}
//重载相等运算符
bool operator==(const Sales_date &a,cosnt Sales_date &b)
{
return a.units_sold==b.units_sold && a.revenue==b.revenue;
}
//重载 != 运算符
bool operator!=(const Sales_date &a,cosnt Sales_date &b)
{
return !(a==b);
}
//重载+=运算符
Sales_date& operator+=(const Sales_date & rhs)
{
units_sold+=rhs.units_sold;
revenue+=rhs.revenue;
return *this; //返回*this即本身
}
实现递增和递减运算符时要区分前置和后置版本
前置运算符返回的是递增或递减后对象的引用,而后置版本则是对象递增递减后返回之前的拷贝,是一个值
为了区分前置后置,在后置中有参数int,前置则无参数。这个int形参无需命名
//前置版本: //执行所需要的递增或递减后,返回*this
Sales_date& operator++()
{
//执行递增操作;
return *this; //返回*this即本身
}
//后置版本:
Sales_date operator++(int)
{
Sales_date tmp=*this; //先存储当前的变量值
//执行递增操作;
return tmp; //返回tmp即可
}
模板与泛型编程
关键字:template
//函数模板
template <typename T>
int compare(const T& v1,const T& v2)
{
if(v1<v2) return -1;
else if(v2<v1) return 1;
else return 0;
}
template <typename T> T foo(T* p)
{
T tmp=p;
// ....
return tmp;
}
//类模板
template <class type> class class-name
{
}
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();
}
};
vector扩容原理说明
新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
c++类型安全
c++远比C更有类型安全性
c++的一些新的机制保障类型安全:
new返回的指针类型严格与对象匹配,而不是malloc返回的void*空类型指针
c++提供了dynamic_cast关键字,使得转换过程更加安全
引入const关键字代替#define constants,有类型和作用域,而#define constants是简单的文本替换;
C++中四种类型转换方式
转换类型操作符 作用
- static_cast 静态类型转换,类似于c的强制类型转换。包括:基本的数据类型转换,int到float等;
- const_cast 去掉类型的const或volatile属性
- dynamic_cast 有条件转换,动态类型转换,运行时检查类型安全(转换失败返回NULL) 通常用于基类和子类之间的转换
- reinterpret_cast 仅重新解释类型,但没有进行二进制的转换,可以进行不同类型的指针转换 该转换较危险,很少使用,慎用
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast 用于将const变量转为非const
2、static_cast 用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。 向上转换:指的是子类向基类的转换 向下转换:指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换? C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
c++11的新特性
auto : auto可以根据上下文推测变量类型 auto声明的变量必须要初始化.
auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响
nullptr:替换NULL,避免NULL可能导致的问题 NULL在C++中代表着0,而nullptr在任何时候都代表空指针。
基于范围的for循环:
vector<int> v{1,2,3,4,5};
for(const auto& e : v)
cout<<e<<endl;
虚函数的override和final指示符
override,表示函数应当重写基类中的虚函数
final,表示派生类不应当重写这个虚函数
智能指针:为防止内存泄露等问题,用一个对象来管理野指针,使得在该对象构造时获得该指针管理权,析构时自动释放。包含在头文件<memory>中
智能指针名称
auto_ptr 基于所有权转移。 ( 已经被c++11抛弃)缺点:一个空间不能由两个auto_ptr管理,不然会析构两次;auto_ptr的拷贝构造会将原指针的管理权交给目标指针,会使得原指针悬空。故auto_ptr会导致一些内存泄漏和野指针问题
unique_ptr “唯一”拥有其所指对象,保证同一时间内只有一个智能指针可以指向该对象。有效避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”) 不共享它的指针,无法复制到其他unique_ptr(无拷贝构造函数),无法通过值传递到函数(无拷贝构造)
shared_ptr 允许多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用(当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏)
weak_ptr 不增加计数,为了解决shared_ptr存在相互引用的问题,确保能够正确析构。是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
STL
容器名称
- unordered_map 采用HASH MAP实现,如果没有顺序遍历需求采用unordered_map通常更优。
- map内部实现了一个红黑树,故查找时间复杂度为O(logn),
- unordered_map通过哈希映射实现查找复杂度为O(1));
unordered_set 采用HASH MAP实现(set采用红黑树实现)
array 具有固定大小的数组。支持快速随机访问。不能添加或删除元素。array除了有传统数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能
forward_list 单向链表。forward_list和list的区别在于前者是单向链表,它的迭代器是前向有效的;后者是双向链表,在内部存在两个链接,它的迭代器是双向有效的。
malloc/free 和new/delete
相同点:都可用于申请动态内存和释放内存
区别:malloc只分配指定大小的堆内存空间,而new可以根据对象类型分配合适的堆内存空间。free释放对应的堆内存空间,delete,先执行对象的析构函数,在释放对象所占空间。malloc分配时的大小是人为计算的,返回类型是void*,使用时需要类型转换,new在分配时,编译器能够根据对象类型自动计算出大小,返回类型是指向对象类型的指针。new调用构造函数构造对象,而malloc不能;delete将调用析构函数析构对象,而free不能
程序的内存分配
栈区:由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等
堆区:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收
全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放
常量区(文字常量区):存放常量字符串,程序结束后有系统释放
代码区:存放函数体(类成员函数和全局区)的二进制代码
内存分配方式
从静态存储区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
在栈上创建:在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束时,这些内存单元会自动被释放。栈内存分配运算内置于处理器的指令集,效率高,但是分配的内存容量有限
从堆上分配:程序在运行的时候使用malloc或者new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。如果在堆上分配了空间,既有责任回收它,否则运行的程序会出现内存泄漏,频繁的分配和释放不同大小的堆空间将会产生内存碎片
C++类中数据成员初始化顺序
1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
3.类中const成员常量必须在构造函数初始化列表中初始化。
4.类中static成员变量,只能在类内外初始化(同一类的所有实例共享静态成员变量)。
初始化顺序:
1) 基类的静态变量或全局变量
2) 派生类的静态变量或全局变量
3) 基类的成员变量
4) 派生类的成员变量
虚函数的实现
一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针
虚函数构造过程:
先拷贝父类的虚函数表
替换已重写的虚函数指针
增加自己再定义的虚函数指针
请你说说动态内存分配
在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配对象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配对象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
C++基础知识面试必备、复习细节 (4) {Effective c++}
尽量以const,enum,inline替换#define
const double Ratio=1.65; //用该方式定义常量
#define Ratio 1.65; //尽量不要以define方式定义常量
//在类中,也可以用enum替换define
class GamePlayer
{
enum{NumTurns=5}; //NumTurns就成为5的一个记号
int scores[NumTurns];
}
//采用inline函数方式替换#define函数
//#define函数经常会引发难以察觉的错误
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b)) //用宏写函数最好为所有实参加上括号
//但还是有一些问题
int a=5,b=0;
CALL_WITH_MAX(++a,b); //a会被累加两次
CALL_WITH_MAX(++a,b+10); //a会被累加一次 //这些错误难以发觉
template<typename T>
inline void callWithMax(const T&a,const T&b)
{
f(a>b?a:b);
}
总之,对于单纯常量,用const对象或enum替换#define
对于宏函数,采用inline函数替换
尽可能使用const
- const出现在星号左边,表示被指物是常量;int const *p; 等价于 const int *p;
- const出现在星号右侧,表示指针自身是常量;int * const p
两边都有表示被指物和指针都是常量
在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。
(1)有 const 修饰的成员函数(指 const 放在函数参数表的后面),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
(2)常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。
当non-const成员函数和const成员函数有相同的等价实现时,令non-const函数调用const版本避免代码重复
确定对象被使用前已先被初始化
确保每一个构造函数都将对象的每一个成员初始化。尽量采用初始化列表的方式进行初始化。
为了避免"跨编译单元之初始化次序"问题,以local static对象替换non-local static对象
了解C++默默编写并调用哪些函数
编译器会默默为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数
若不想使用编译器自动生成的函数,就应该明确拒绝
为了驳回编译器自动提供的函数,可以将对应的成员函数声明为private并且不予以实现
在C++11中可以直接在成员函数声明后面添加“= delete”,就可以阻止调用。
为多态基类声明virtual析构函数
带有多态性质的base class应该声明一个virtual析构函数,不然对其base类型指针析构时,可能会出现局部销毁的情况,因为无法调用到derived class的析构函数。如果classes设计目的没有打算作为base classes,或者不是为了具备多态性,那么就不应该声明virtual析构函数
别让异常逃离析构函数
析构函数不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或结束程序。
会抛出异常的代码不要放在析构函数中
不要在构造和析构过程中调用virtual函数
构造函数和析构函数中不要调用任何虚函数,也不要调用那些调用虚函数的函数。更加根本的理由在于:在derived class对象的base class构造期间,对象的类型是base class而非derived class
令operator=返回一个reference to *this
令赋值操作符返回一个reference to *this 可以实现一个连锁赋值,保持和内置类型、标准库一致。
在operator=中处理“自我赋值”
处理赋值时将自己赋值给自己的情况
复制对象时勿忘其每一个成分
copying函数应该确保复制”对象内所有的成员变量“及”所有base class成分“
不要尝试以某个copying函数去实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用
以对象管理资源
将资源放进对象内,利用析构函数自动调用机制确保资源被释放。尽量使用智能指针
在资源管理中小心copying行为
在资源管理中提供对原始资源的访问
成对使用new和delete时要采取相同形式
如果在new表达式中使用[],delete也要使用[];如果new中不使用[],delete中也不要使用[]。特别注意typedef,很具迷惑性。
以独立语句将newed对象置入智能指针
以独立语句将newed对象存储于智能指针内,不然,一旦异常抛出,有可能导致难以察觉的资源泄露。即不要在给函数传参数时直接定义临时(匿名)的智能指针。应该先定义好智能指针对象,再将该对象传递给函数。
让接口容易被正确使用,不易被误用
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质
促进正使用的办法包括接口的一致性,以及与内置类型的行为兼容。
阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
设计class犹如设计type
设计类时有很多问题需要思考,最好整个框架定好后再开始设计,并根据需求进行完善。
以pass-by-reference-const替换pass-by-value
pass-by-value会造成较多的构造函数与析构函数的开销,采用pass-by-reference-const可以避免该开销,必须加上const
对于内建类型和STL迭代器、函数对象,pass-by-value是高效的
不要返回临时对象的引用
不要返回一个临时对象的引用或指针
不要返回堆上分配对象的引用
将类中变量声明为private
C++基础知识面试必备、复习 Effective c++
- 尽可能延后变量定义式的时间
- 尽可能推迟定义变量,只有在真正要使用的时候再定义,最好在需要初始化时定义,即定义时就立刻初始化。
- 尽量少做转型动作
- 尽量避免使用cast,必须使用cast的情况下,应尽量将cast置于函数内部
- 避免返回一个指针、引用或者迭代器指向类内的成员
- 原因是如果返回了成员的引用或者指针,就可以通过这个引用或者指针修改类内的private成员
- 努力处理异常
- 当异常被抛出时,要做到不泄露任何资源,不允许数据败坏。
- 尽量将inline用在小型、被频繁调用的函数身上,便于之后调试,同时提升程序速度的同时减小代码膨胀问题
- inline只是一种申请,编译器会根据具体情况来决定一个函数是否可以是inline得,比如递归函数、virtual函数、代码较多的函数,即使你声明了inline关键字,编译器也不会将此类函数视为inline的函
- 将文件间的编译依存关系降至最低
- 尽量让头文件不依赖其他文件,以对声明的依赖取代对定义的依赖
- 确保public继承表示的是"is a"关系
- 也就是说,派生类是一个基类,基类上的每一个事情都使用于派生类
- 名称遮掩问题
- 子类会遮掩父类同名的函数,可以使用类名作用域决定调用父类还是子类的函数。
- 区分接口继承和实现继承
- 在public继承下,派生类总是会继承基类的接口。
- 对于纯虚函数只继承接口,子类必须重载该函数;对于非纯虚函数继承接口并存在默认的实现继承,普通函数则继承接口及并强制继承其实现。
- 考虑virtual函数以外的其他选择
- 不要重新定义继承而来的非虚函数
- non-virtual在实现上是静态绑定的,调用父类还是子类的函数完全取决于指针或者对象的类型。在子类重定义non-virtual时,父类的相同的函数是不会被覆盖的
- 不要重新定义继承而来的默认参数
- 通过复合composition塑造出has-a或"根据某物实现出"
- 在应用域,复合意味着has-a,在实现域,符合以为着 根据某物实现出
明确私有继承
由private base class继承而来的所有成员,在derived class中都会变成private属性
慎重使用多重继承
了解typename
声明template参数时,前缀关键字class和typename可以互换
学习处理模板化基类内的名称
在子类模板中,如果要引用父类模板的name时,用this->name、using Base<T>::name声明或者基类限定(Base<T>::name)的方式。
将与template参数无关的代码抽离到模板外
原因是模板会根据具体类型具象化不同的代码,如果将与模板无关的代码也放入模板函数或者类中,那么就会生成重复的代码,就会导致代码膨胀的问题,函数模板中与参数无关的代码可以包装成单独的函数。类模板中与参数无关的模板可以放到父类中。
不要轻易忽视编译器的警告
严肃对待编译器的警告,也不要过度依赖于编译器的报警能力。
C++构造函数用初始化列表进行初始化和构造函数体内赋值进行初始化的区别?
构造函数初始化列表和构造函数体最大的区别是初始化列表是初始化,而函数体内是赋值操作;
对于普通的数据类型两种操作只有资源消耗的区别。但引用和const常量都是不能被赋值的,它们在类内只能在构造函数的参数初始化列表中被初始化。
对于对引用变量和const变量的初始化问题:
在进入构造函数体内时,实际上变量都已经初始化完毕了,即引用变量和const变量都已经用不确定的值初始化好了,构造函数内能做的只有赋值,而const类型和引用类型是不可以赋值的。所以,需要在初始化列表中初始化。
挂起和阻塞区别:
(1)挂起是一种主动行为,因此恢复也应该要主动完成。而阻塞是一种被动行为,是在等待事件或者资源任务的表现,你不知道它什么时候被阻塞,也不清楚它什么时候会恢复阻塞。
(2)阻塞(pend)就是任务释放CPU,其他任务可以运行,一般在等待某种资源或者信号量的时候出现。挂起(suspend)不释放CPU,如果任务优先级高,就永远轮不到其他任务运行。一般挂起用于程序调试中的条件中断,当出现某个条件的情况下挂起,然后进行单步调试。
sleep()和wait()函数的区别:
(1)两者比较的共同之处是:两个方法都是使程序等待多少毫秒。
(2)最主要区别是:
- sleep()方法没有释放锁。
- wait()方法释放了锁,使得其他线程可以使用同步控制块或者方法。
(3)sleep()指线程被调用时,占着锁不工作,形象的说明为“占着锁”睡觉。
sleep(2000)表示:占着锁,程序休眠2秒。
wait(2000)表示:不占用CPU,程序等待2秒。
聊聊map和set的区别,底层都是怎样实现的?
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。
由于map和set所开放的各种操作接口,RB-tree也都提供了,所以几乎所有的map和set的操作行为,都只是转调RB-tree的操作行为。
map和set区别在于:
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
(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。
set是一种关联式容器,其特性如下:
- set以RBTree作为底层容器
- 所得元素的只有key没有value,value就是key
- 不允许出现键值重复
- 所有的元素都会被自动排序
- 不能通过迭代器来改变set的值,因为set的值就是键
map和set一样是关联式容器,它们的底层容器都是红黑树,区别就在于map的值不作为键,键和值是分开的。它的特性如下:
- map以RBTree作为底层容器
- 所有元素都是键+值存在
- 不允许键重复
- 所有元素是通过键进行自动排序的
- map的键是不能修改的,但是其键对应的值是可以修改的
函数指针:
1、定义 函数指针是指向函数的指针变量。 函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。
这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。 C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
2、用途: 调用函数和做函数的参数,比如回调函数。
3、示例: char * fun(char * p) {…} // 函数fun char * (*pf)(char * p); // 函数指针pf pf = fun; //函数指针pf指向函数fun pf(p); //通过函数指针pf调用函数fun
请你讲讲如何采用单线程的方式处理高并发?
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,
然后再采用事件驱动模型,基于异步回调来处理事件来 。
请说说C++11 最常用的新特性:
1) auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
2) nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
3) 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
4) 初始化列表:使用初始化列表来对类进行初始化 。
5) 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率 。
6) atomic原子操作用于多线程资源互斥操作
7) 新增STL容器array以及tuple。
这个博客写的很好:
https://blog.csdn.net/u012864854/article/details/79777991
很牛的一个C++小结
https://www.cnblogs.com/liufei1983/p/7099401.html
c++复习5篇
https://blog.csdn.net/dingdingdodo/article/details/106356981
https://blog.csdn.net/dingdingdodo/article/details/106422693
https://blog.csdn.net/dingdingdodo/article/details/106434067#comments
https://blog.csdn.net/dingdingdodo/article/details/106451930
https://blog.csdn.net/dingdingdodo/article/details/106462321
C++17新属性详解
https://blog.csdn.net/fanyun_01/article/details/80471626
https://www.jianshu.com/p/f880702ad709
C++11常用特性的使用经验总结
https://www.cnblogs.com/feng-sc/p/5710724.html
[c++11]我理解的右值引用、移动语义和完美转发
https://www.jianshu.com/p/d19fc8447eaa
100条经典C++语言笔试题目-前50题
https://blog.csdn.net/sinat_20265495/article/details/53442679
100条经典C++语言笔试题目-后50题
https://blog.csdn.net/sinat_20265495/article/details/53443218
C++ STL容器底层实现原理
https://blog.csdn.net/weixin_38337616/article/details/88587388
STL笔试面试题总结(干货)
https://blog.csdn.net/zzb2019/article/details/81195294
虚函数表详解
https://blog.csdn.net/Primeprime/article/details/80776625
https://blog.csdn.net/lihao21/article/details/50688337
C/C++ 笔试、面试题目大汇总
https://blog.csdn.net/xjbclz/article/details/51824272
C++面试题( 收集并整理)
https://blog.csdn.net/xjbclz/article/details/51834667
笔试题目总结之四——各种排序算法
https://blog.csdn.net/sea1105/article/details/50405892
笔试题目总结之三——软件工程中的开发模式
http://topic.csdn.net/u/20080929/18/e8fe492b-c8f3-46c8-8bd0-9cec0aca52f5.html
https://blog.csdn.net/sea1105/article/details/50405892
笔试题目总结之二——常用数据结构与算法
https://blog.csdn.net/sea1105/article/details/50405879
多线程经典面试题
https://blog.csdn.net/mandagod/article/details/79424454
C++笔试面试常考知识点汇总(一) 从一到五
https://blog.csdn.net/hmxz2nn/article/details/53151772
程序员经典电子书下载(超全)
https://blog.csdn.net/sunxiaosunxiao/article/details/6630448
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)