layout: post
title: 八股总结(一)C++语法、内存管理、新标准、STL
description: 八股总结(一)C++语法、内存管理、新标准、STL
tag: C++
文章目录
- 基础语法
- 语言特性
- 面向对象的三大特性?
- C++中新增了string,它与C语言中的char*有什么区别吗?它是如何实现的?
- 如何用代码判断大小端存储
- 网络字节序与主机字节序
- C和C++的类型安全
- 什么是类型安全?
- C的类型安全
- extern"C"的用法
- 关键字
- 重载(overload)、重写(override)、隐藏(hide)
- override和final
- volatile、mutable和explicit关键字用法
- 宏定义与静态变量
- 内联函数和宏定义的区别
- 内联函数inline适用的场景
- define宏定义和const的区别?
- const和static的作用
- 指针与引用
- 一个指针占多少字节?
- 指针与引用的区别与联系?
- 在传递函数参数,如何决定是使用指针还是引用?
- 顶层const与底层const的区别?
- 数组名和指针(指向数组首元素的指针)的联系与区别?
- 野指针和悬空指针定义、解决办法。
- 类与模板
- struct与class的区别?
- C++有哪几种构造函数?
- 成员初始化列表的概念,用它为什么会快一些?
- public、protected和private访问权限和继承权限
- 内存管理与拷贝控制
- 内存安全管理
- 内存分区
- 堆与栈的区别
- 什么是内存泄露,如何避免与检测
- new / delete / 与malloc / free的异同
- 拷贝控制
- 浅拷贝和深拷贝的区别
- 初始化和赋值的区别
- 什么情况下会调用拷贝构造函数。
- 说说移动构造函数
- 异常处理
- 常见错误类型
- 写C++代码时有一类错误是 coredump ,很常见,你遇到过吗?怎么调试这个错误?
基础语法
语言特性
面向对象的三大特性?
- 封装:将客观事物封装为抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不信的进行信息隐藏。
- 继承:使用现有类的所有功能,无需重写,直接对基类进行功能拓展延伸。
- 多态:同一事物可以表现为不同事物的能力,不同对象在接收时产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。实现多态有两种方式:使用override(重写)和使用overload(重载)。
C++中新增了string,它与C语言中的char*有什么区别吗?它是如何实现的?
string继承自basic_string,其实是对char进行了封装,封装的string包含了char数组,容量、长度等属性。
string可以动态拓展,每次拓展时,另外申请了一块原空间两倍大小的空间,然后将原字符串拷贝过去,并加上新增的内容。
如何用代码判断大小端存储
大端存储:数据高位存在低地址中。
小端存储:数据低位存在低地址中。
以unsigned int value = 0x12345678
为例:
在小端模式下:随着地址增长,存放的是更高位的数据,因此高地址的0x12是数据的最高位数据。
使用代码判断大小端机器的方法:int整形强转为char型
由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分,低地址部分对应数据低位,说明是小端机器,否则为大端机器
#include <iostream>
using namespace std;
int main()
{
int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
网络字节序与主机字节序
主机字节序:
就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。小端/大端的区别是指低位数据存储在内存低位还是高位的区别。其中小端机器指:数据低位存储在内存地址低位,高位数据则在内存地址高位;大端机器正好相反。
网络字节序:
4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是大端。
由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。
C和C++的类型安全
什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。
C的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体指针转为另一种结构体指针时,编译器将会报告错误,除非使用显式类型转换。然而C中相当多的操作是不安全的。以下是两个十分常见的例子:
-
printf格式输出
-
malloc的函数返回值
malloc是C中进行内存分配的函数,它的返回类型是void*即空类型指针,常常有这样的用法char* pStr=(char*)malloc(100*sizeof(char))
,这里明显做了显式的类型转换。
类型匹配尚且没有问题,但是一旦出现int* pInt=(int*)malloc(100*sizeof(char))
就很可能带来一些问题,而这样的转换C并不会提示错误。
####C++的类型安全
如果C++使用得当,它将远比C更有类型安全,相对于C语言,C++提供了新的机制保障类型安全:
- 操作new返回指针类型严格与对象匹配,而不是void*
- C中很多void*为参数的函数可以改写为C++函数模板,而函数模板是支持类型检查的。
- 引入了const关键字,代替#define constants,它是有类型,有作用域的,而#define constants只是简单的文本替换。
- 一些#define宏,可以被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保障类型安全。
- C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
extern"C"的用法
为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;
哪些情况下使用extern “C”:
(1)C++代码中调用C语言代码;
(2)在C++中的头文件中使用;
(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;
关键字
重载(overload)、重写(override)、隐藏(hide)
- 重载(overload):在同一范围定义中的同名成员函数才存在重载关系。特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否为虚函数无关。
- 重写(override):重写指的是在派生类中覆盖基类中的同名虚函数,重写就是重写函数体,要求基类函数必须是虚函数且与基类的虚函数有相同的参数个数、参数类型和返回值类型。
重载和重写的区别:
- 重写是父类与子类之间的垂直关系,重载是同名不同参数的函数之间的水平关系。
- 重写要求参数列表相同,重载则要求参数列表不同,返回值不同。
- 重写关系中,调用方法根据对象类型决定,重载根据调用时实参与形参表的对应关系选择函数体。
- 隐藏(hide)
隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包含以下情况:
- 两个函数参数相同,但是基类函数不是虚函数,和重写的区别在于基类函数是否为虚函数。
- 两个函数参数不同,无论基类函数是否为虚函数,都会被隐藏,和重载的区别在于,两个函数不在同一个子类中。
override和final
当在父类中使用虚函数时,子类可以对这个函数进行重写。
class A
{
virtual void foo();
}
class B : public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}
使用override指定重写父类的虚函数,编译器可以帮助我们检查父类中是否有该虚函数。
比如,假定我们将foo写成了f00,有override的话,编译器发现父类中没有该虚函数不会通过编译,而不加override,会被认定为子类的新的方法,造成错误。
当不希望某个类被继承、或者不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。
volatile、mutable和explicit关键字用法
- volatile(易变的、不稳定的)关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程。
遇到volatile关键字,编译器对访问该变量的代码不再进行优化,从而可以提供对特殊地址的稳定访问。volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型
volatile指针:volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念
修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;volatile char* vpch;
修饰指针自身的值——即地址本身是const或者volatile的:
char* const pchc;char* volatile pchv;
-
mutable(可变的,易变的),C++中mutable是为了突破const的限制而设置的,被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数。
-
explicit
explicit(明确的)关键字用来修饰类的构造函数,被修饰构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换。
注意:
- explicit关键字只能用于
类内部的构造函数的声明
- explicit关键字作用于单个参数的构造函数
- 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换。
宏定义与静态变量
内联函数和宏定义的区别
- 在使用时:宏只做简单字符串替换,而内联函数可以进行参数类型检查且具有返回值。
- 内联函数在编译时直接将函数代码嵌入到目标代码中,省去了函数调用时的开销,以提高执行效率,并且进行参数类型检查,有返回值,可以实现重载。
- 宏定义时要注意书写(参数括起来),否则容易出现歧义,内联函数不会产生歧义。
内联函数inline适用的场景
- 使用宏定义的地方都可以使用 inline 函数。
- 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
define宏定义和const的区别?
- 编译阶段上:
define是在编译的预处理阶段起作用,而const是在编译、运行时候起作用。 - 安全性上:
define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加一个大括号包住全部的宏定义内容,不然容易出错,而const是常量是有数据类型的,编译器可以对它进行安全检查。 - 内存占用上:
define只是将宏名称进行替换,在内存中会产生多份相同的备份,const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表。
const和static的作用
static
-
不考虑类的情况:
- 隐藏的功能,不加static的全局变量和全局函数具有全局可见性,可以在其他文件中使用,加了static之后的全局变量和全局函数只能在该文件所在的编译模块中使用。
- 默认初始化为0,存储在静态数据区,省去了置零的操作。
- 保存数据内容的持久化,具备记忆性。在程序的执行路径第一次经过对象定义语句的时候初始化,并且直到程序终止才被销毁,期间的任务数据修改都具有持久记忆性。(不会因为退出函数等被销毁,除非再次被修改,否则直到程序结束前,值都是不变的)。
-
考虑类的情况:
- static静态成员函数和静态成员不与类对象绑定在一起,所有类对象的静态成员共享一份内存数据。不由类的构造函数初始化,因此
必须在类的外部定义和初始化每个静态成员
- static静态成员函数不具有this指针(因为不与类对象绑定,所有类对象共享访问),static成员函数内不可以访问非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile.
- static静态成员函数和静态成员不与类对象绑定在一起,所有类对象的静态成员共享一份内存数据。不由类的构造函数初始化,因此
const
- 不考虑类的情况:
- const常量在定义时必须初始化,初始化后不能修改。
- const形参可以接收const和非const类型实参
- 考虑类的情况:
- const成员变量:
不能在类定义外部初始化
(与static静态成员变量相反),只能通过构造函数初始化成员列表进行初始化,并且必须有构造函数,不同类对象的const数据成员值可以不同,所以不能在类中声明时初始化。 - const成员函数:const对象不可以调用非const成员函数,不可改变非mutable数据的值。
- const成员变量:
指针与引用
一个指针占多少字节?
64位的编译环境下,指针占用大小为8(8 ×8 = 64)个字节,在32位编译环境下,指针占用大小为 4 (4 ×8 = 32)个字节。
指针与引用的区别与联系?
- 指针是一个变量,存储的是一个地址,引用是变量的别名,跟原变量是一个东西。
- 指针可以有多级,即可以定义指针的指针,而引用则不能。
- 指针可以为空,可以初始化为null,初始化后还可以改变指针的指向,而引用则必须在定义时初始化,一旦初始化绑定变量后,就不可再改变。
- 指针与引用作为参数传递时,都可以改变实参的值,不同的是,指针作为参数传递时,是将指针的一个拷贝传递给形参,两者指向相同地址,但不是同一个变量,在函数中改变这个变量的指向,不会影响实参,但引用可以。
- 引用的本质是一个指针常量(指针中的常量),指向不可改变。
在传递函数参数,如何决定是使用指针还是引用?
- 需要返回函数内局部变量的内存的时候使用指针,指针传参需要开辟内存,用完需要释放指针,否则会内存泄露,而返回局部变量的引用是没有意义的。
- 对栈空间大小敏感(比如递归)的时候使用引用,不需要创建临时变量,内存开销小。
- 类对象作为参数传递的时候,使用引用,这是C++类对象传递的标准方式。
顶层const与底层const的区别?
- 顶层const: *const,指针常量,指针中的常量,指向不可修改。
- 底层const:const (int)✳, 常量指针,常量的指针,所指对象视为常量,不可改变所指对象。
int i =0;
int *const p1 = &i; // 顶层const,不可改变p1
const int ci = 42;
const int *p2 = &ci; // 允许改变p2,不可改变ci,这是底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的const是底层const
const int &r = ci; // 用于声明引用的const都是底层const
当执行对象的拷贝操作时,顶层const和底层const区别明显。其中顶层const不受影响,底层const的限制不可忽视。一般来讲,非常量可以转为常量,而常量不可转为非常量。
i = ci; // 正确
p2 = p3; // p2和p3所指对象类型相同,p3顶层const的部分不受影响。
int *p = p3; // 错误,p3包含底层const的含义,即所指对象不可修改,而p没有,如果这样定义,可能出现通过p修改常量的错误操作
p2 = p3; // 正确,p2和p3都是底层const
p2 = &i; // 正确,int * 可以转成 const int *
int &r = ci; // 错误,普通的int不可以绑定到int常量上,如果这样做,可能出现通过r修改常量ci的错误操作
const int &r2 = i; // 正确,const int & 可以绑定到一个普通的int上。
事实上底层const出现限制的原因主要在于由于底层const所指对象为常量,故绑定引用或者给指针赋值时,可能出现通过该引用或指针去修改常量的后果,出现这种后果的拷贝操作都是错误的
数组名和指针(指向数组首元素的指针)的联系与区别?
- 二者均可以通过增减偏移量来访问数组中的元素。
- 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减操作。
- 数组名当做形参传递给调用函数后,就失去了常指针的特性,退化为一般指针,可以自增、自减,且
sizeof
运算符不再能得到原数组的大小了。
野指针和悬空指针定义、解决办法。
- 野指针:没有初始化的指针
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
- 悬空指针:指针最初指向的内存已经被释放的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
- 产生原因和解决方法:
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空(= nullptr)。
悬空指针:指针free或delete后没有及时置空 =>释放操作后立即置空。
类与模板
struct与class的区别?
- 相同点
- 两者都拥有成员函数、公有和私有部分
- 任何可以使用class完成的工作,同样可以使用struct完成
- 不同点
-
两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
-
class默认是private继承, 而struct默认是public继承
成员类别上,struct默认成员为public,class默认成员为private;继承关系上,struct默认公有继承,private默认私有继承。
-
C++有哪几种构造函数?
- 默认构造函数
- 初始化构造函数(有参数)
- 拷贝构造函数
- 移动构造函数(move和右值引用)
- 委托构造函数
- 转换构造函数
成员初始化列表的概念,用它为什么会快一些?
在类的构造函数中,不在函数体内对成员变量赋值,而是 在花括号前边使用冒号和初始化列表赋值。
用它会快一些的原因是,对于有类成员的情况,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用,(函数体中需要一次默认构造,加一次赋值,而初始化成员列表只做一次赋值操作),而对于内置数据类型成员则没有差异。
public、protected和private访问权限和继承权限
- public成员类内和类外都可以访问,protected成员只能在类内或者派生类中访问,private的成员只能在类内访问。
内存管理与拷贝控制
内存安全管理
内存分区
C++程序在执行时,将供用户使用内存大致划分为四个区域:
(1)代码区:存放函数体的二进制代码,由操作系统进行管理;
(2)全局区:存放全局变量和静态(全局、局部)变量和字符串常量;
(3)栈区(stack):由编译器自动分配释放, 存放函数的参数值,局部变量等;
(4)堆区(heap):由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
堆与栈的区别
- 申请方式不同:堆区是自行申请和释放,栈区是系统分配和释放。
- 申请空间大小、排布、限制不同:栈区是一片连续的空间,大小由操作系统预定好,在Windows下是2M,空间较小,由高地址向低地址生长,如果申请空间大于剩余空间,分配失败,栈溢出。堆区在内存中是不连续的(系统使用链表存储空闲内存地址,自然是不连续的),堆大小受限于计算机系统中的有效虚拟内存(32位机理论上为4G),堆区较大。
- 申请效率不同:栈有系统自动分配,申请效率高,堆由开发者申请,效率低,且容易产生内存碎片。
形象的比喻
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
什么是内存泄露,如何避免与检测
内存泄露:
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了
避免内存泄露的几种方式:
- 计数法:使用new或者malloc时,让该数加1,delete或free时,该数减1,程序执行完毕时打印这个数,如果不为0,表示存在内存泄露。
- 一定要将基类中的析构函数声明为虚函数。
- 对象数组的释放一定要使用delete[]
- 有new就有delete,有malloc就有free,保证他们成对出现。
new / delete / 与malloc / free的异同
- 相同点
- 都可以用于内存的动态申请和释放
- 不同点
- 从定义上讲,new与delte是C++运算符,malloc和free是C++标准库函数,因此new与delete,不依赖外部库函数文件,malloc和free则需要依赖外部库函数文件。
- 从使用上讲, new自动计算分配空间大小,malloc申请的空间大小需要手工计算; new是类型安全的,malloc不是。
- 从执行过程上讲:
new调用名为operator new
的标准库函数,分配足够空间,并调用相关对象的构造函数,delete对指针所指对象运行对应的析构函数,然后通过调用名为operator delete
的标准库函数释放该对象所用的内存。malloc与free均没有相关的调用。new 是封装了malloc,直接free不会报错,但只是释放内存,不会析构对象。
int *p = new float[2]; // 编译错误
int *p = (int*)malloc(2 * sizeof(double)); // 编译无错误
拷贝控制
浅拷贝和深拷贝的区别
- 浅拷贝:浅拷贝只是拷贝一个指针,并没有开辟一个地址,拷贝的指针和原来的指针指向同一块的地址,如果原来的指针所指向的资源被释放了,那么再释放浅拷贝的指针的资源就会出现错误。
- 深拷贝:深拷贝不仅拷贝值,还开辟了一块新的空间用于存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝的值,在自己实现拷贝赋值时,如果有指针成员变量,是需要自己实现深拷贝的。
初始化和赋值的区别
- 对于简单类型而言,初始化和赋值没有区别。
- 对于类和复杂数据类型(指针)而言,两者区别很大,类可能重载了赋值运算符。
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}
什么情况下会调用拷贝构造函数。
- 用类的一个实例化对象去初始化另一个对象时。
- 函数的参数是类的对象时(非引用传递)
- 函数的返回值是函数体局部对象的类时,返回值调用拷贝构造函数。
说说移动构造函数
- 移动构造函数设计的初衷:当我们用对象a初始化对象b后,对象a不再使用了,但是对象a的空间还在(没有析构),既然拷贝的目的是为了把a对象的内容复制一份给b,那么使用移动构造函数能够直接使用a的空间,避免新空间的分配,大大降低了构造成本。
- 拷贝构造函数中,对于指针我们一定要采用深拷贝,而移动构造函数中,对于指针,我们采用浅拷贝,浅拷贝之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。
- 移动构造函数的参数和拷贝构造函数不同,拷贝构造参数是一个左值引用,移动构造函数的参数是一个右值引用。
异常处理
常见错误类型
写C++代码时有一类错误是 coredump ,很常见,你遇到过吗?怎么调试这个错误?
coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。