1:字节对齐
(1)自身对齐值:每种数据类型,都有自己的对齐值,基础数据类型的对齐值就是其字节数,结构体类型的对齐值是其成员变量中对齐值最大的那个。
(2)指定对齐:使用 #pragma pack (value) 可以指定对齐值value。
举个例子:
struct B{
7 char b;
8 int a;
9 short c;
10 };
这个结构体一共占12个字节,为什么呢?
b的对齐值为1,因此占用第一个字节,a的对齐值为4,地址起始值为4的倍数,因此从第4个字节存起到4+4=8,c的对齐值为2,8是二的倍数,8+2=10。B本身的对齐值是其中对齐值最大的元素a的对齐值4。因此B最终占用的字节数要是4的倍数,且大于10,就是12了。
2:内存拷贝
这个题之前头条问过,当时没有考虑内存重叠的情况。
这里注意几个点:
- 是src前要加const,属于好的编码习惯。
- void*,万能指针,转化为char*之后,加1时一次移动一个字节。
- 内存根据不同的重叠方式,选择从后往前或从前往后拷贝。
void* memcpy(void* des,const void* src,size_t count)
{
if (!src || count <= 0) return des;
char* td = (char*)des;
const char* ts = (const char*)src;
//如果源地址 + count 小于目的地址或者目标在复制过程中可能会和原地址重叠,正向拷贝
if (ts + count < td || td + count > ts)
{
while (count--) *td++ = *ts++;
}
//否则进行逆向拷贝
else
{
char* ttd = td + count - 1;
const char* tts = ts + count - 1;
while (count--) *ttd-- = *tts--;
}
return des;
}
3:static关键字
主要注意点三条
- 无论在函数外部还是内部,都只会被初始化一次
int add(){
static int a = 0;
return ++a;
}
int main(){
cout << add() << add() << add() << endl;
}
输出 1 2 3
- 无法被其他文件直接使用,如果需要,只能借助于非static类型变量
文件A中:
static char C = 'A';
char c = C;
static int F(){
return 5;
}
int f(){
return F();
}
文件B中:通过f和c,间接引用F和C
int main(){
extern int f();
extern char c;
cout << f() << endl;
cout << c << endl;
return 0;
}
- 最后这条就简单了,类中的static类型属于类
4: 继承、虚函数、纯虚函数
c++继承和其他语言有个很大的区别在于,他的继承分为 private public protect。
(1)子类会继承父类的所有非静态成员。
(2)继承成员的暴露情况依据继承方式而定,如父类是private,继承成员都是private,如父类是protect,继承成员最高为protect。
(3)成员变量寻找,遵循 子类->子类中的父类属性 的顺序,在子类中找到,即使为private,不能访问,也不会再去父类中查找了。
虚函数和多态
多态,往简单的说,就是父类指针调用子类对象。
不使用虚函数的情况下,c++是没有多态的,比如这个例子。
class Person{
public:
void say(){
cout << "person" << endl;
}
};
class Student:public Person{
void say(){
cout << "student" << endl;
}
};
int main(){
Student st;
Person& p = st;
p.say();
return 0;
}
输出 person。这是因为c++静态语言的特性,编译的时候就已经确定好了。
c++中虚函数的作用一共有两个:
(1)实现多态的特性。
class Person{
public:
virtual void say(){
cout << "person" << endl;
}
};
class Student:public Person{
void say(){
cout << "student" << endl;
}
};
int main(){
Student st;
Person& p = st;
p.say();
return 0;
}
由于父类的say是虚函数,在运行时,会先执行子类的say方法,如果子类没有say,才会去虚函数表中查找。
关于虚函数表,具体可以参考这篇文章:
https://blog.csdn.net/haoel/article/details/1948051
其中有提到,如果子类的虚函数覆盖了父类的虚函数,使用子类调用时,子类虚函数的优先级也在父类之上。
(2)纯虚函数和抽象类。
纯虚函数就是没有函数体的虚函数,具体声明方式如下:
class Base{
public:
virtual void f() = 0;
};
具体实现就是虚函数表中,指向函数体的指针为0,空的。
包含纯虚函数的类,就是抽象类,无法被实例化。
但基本上,只有成员函数才能够声明为虚函数,也只有如此才有意义,以下几种函数是无法声明为虚函数的:
普通函数(非成员函数)
静态成员函数
内联成员函数
构造函数
友元函数。
(3) 虚析构
class Person{
public:
virtual ~Person(){
cout << "person die" << endl;
}
};
class Student:public Person{
public:
~Student(){
cout << "student die" << endl;
}
};
在父类中加上virtual,父类和子类的析构函数都变成虚析构了,那么虚析构有什么用呢?
在多态下,使用父类指针指向了子类对象,如:
Person *p = new Student;
delete p;
释放p的时候,如果父类不是虚析构,只会调用父类的析构函数,但由于真实指向的是子类,这么做难免清理不干净内存。如果使用虚析构,由于c++的动态联调,会同时调用子类和父类的析构函数,达到清理的目的。
如上面的例子,最终会打印:
student die
person die
c++中有两种多态,静态多态在编译时确认,用的泛型,动态多态在调用时确认,用的就是虚函数了。
5: 棱形继承问题和虚继承
class A{
protected:
int a;
};
class B : virtual public A{
int b;
};
class C : virtual public A{
int c;
};
class D : public B,public C{
int d;
public:
void f(){
cout << this->a << endl;
}
};
B C继承于A,于是A的成员变量a在B中有一份,在C中也有一份,D继承了B和C,那么D中的a是用哪个B中的还是C中的呢?这就出现了棱形继承问题。解决的办法就是B C虚继承于A。
另外c++默认继承方式是私有的。
6: 不能重载的几个运算符
.(成员访问运算符)
->(成员指针运算符)
::(作用域解析运算符)
?(条件运算符)
sizeof运算符
7: #include 后面跟引号与尖括号的区别?
引号编译器会搜索当前工作目录下的头文件,尖括号会搜索安装目录下的头文件。
类成员函数的重载、覆盖(重写)区别
重载: 发生在同一个类中,或者同一个文件中,函数名称相同,参数不同,就会发生重载,具体调用哪个,根据参数决定。
覆盖:发生在父类和其子类之间,子类如果有方法的方法名、参数、返回值都和父类相同,就会出现覆盖。
c语言数组初始化问题
在c语言中,下面这种写法是错误的,因为数组不能使用变量初始化,否则编译器在编译的时候,不知道给数组分配多少的内存空间。
void f(int size){
int arr[size] = {0};
}
但是,下面这种写法是正确的,使用模板。
template <class T,int size>
void f(){
int arr[size] = {0};
}
8: c++中没有super关键字
java或者objective c中,都有super关键字,用于指明调用父类的某个方法,但c++中,由于支持多继承,没有super关键字,但可以【父类名::方法名】来调用。
9: extern关键字
文件A中声明了全局变量num
int num = 20;
main.cpp中要使用这个全局变量20
int main(){
extern int num;
cout << num << endl;
}
就这么简单,关于有些博客说如下方法会报错
void f();
int main(){
f();
}
int num = 20;
void f(){
cout << num << endl;
}
因为编译器在执行main的时候,并不知道num的存在,需要在main前用 extern num 声明。这个要看编译器,xcode的llvm不会出现这样的错误。
10: 左值、右值、左值引用、右值引用、move()
c++中所有的值必属于左值、右值两者之一,两者简单的区别就是:
可以取地址的,有名字的就是左值,反之,不能取地址的,没有名字的就是右值(将亡值或纯右值)。
比如 int a = b + c,a就是左值,变量名为a,通过&a可以获取该变量名地址。表达式 b+c、 函数 int func() 的返回值是右值,在其赋值给某一变量前,不能通过变量名找到它, &(b+c) 是无法通过编译的。
关于右值(将亡值):
int val = 10;
(val + 5); <- 这就是一个右值,因为它只是计算的周期驻留在临时的寄存器中,没有固定的内存地址,给右值赋值就会出现错误。
通常来说,函数返回值是右值,如
int& f(){
return 2;
}
f() = 3; <- 报错,Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
f返回的是一个右值,变为左值就会报错,但如下情况是可以的
int global_val = 10;
int& f(){
return global_val;
}
f() = 20;
函数返回的 global_val 是一个全局变量,有自己的内存地址,因此是一个左值。
这种性质在运算符重载的时候,其实很有作用,入
vector<int> v(10,1);
v[2] = 10;
这里的 v[2] 实际上调用的就是运算符重载函数,这里给运算符重载函数赋值了。
左值和右值还有意思的一点就是 a++ 和 ++a,其中 a++ 会创建一个a的拷贝,然后返回的拷贝是一个临时对象,因此是右值,而++a最后返回是是a本身,因此是左值。
左值引用和右值引用
先说概念
int b = 10;
int& a = b; //左值引用,用&表示
int&& c = (b + 1); //右值引用,用&&表示
b+1的值是保存在寄存器中的,右值引用可以让我们直接使用寄存器的值。
move() : 作用是将左值变为右值,先看一下使用方式
string str = "abc";
vector<string>v;
v.push_back(str); //这里会触发拷贝构造,str中使用char[]来保存 “abc”,在拷贝构造中,"abc"会被复制一份。
//但如果str使用完了,就没用了,"abc"的复制就显得多余,这时候就可以使用 移动拷贝构造
//move将str变为了右值,因此调用移动拷贝构造,这样str中的char[]数组的所有权就被转交给了vector中的元素,原先的str中的char[]就变成了空。
//变成了右值,str自然就要消失了
v.push_back(move(str));
移动拷贝构造相当于
class string{
private:
char c[];
public:
//移动拷贝构造,接收值是一个右值引用
string(const string&& str){
this->c = str.c;
str.c = NULL;
}
~string(){
if(this->c != NULL){
delete c[];
}
}
}
move的转换应该还有其他的使用方式,先看到这儿了。
11: NULL和nullptr的区别
应该使用nullptr而不是NULL,因为NULL是0,在函数调用的时候会有二义性问题。
NULL在c中的定义是:
#define NULL ((void *)0)
也就是说NULL实际上是一个void *的指针,然后吧void *指针赋值给类似 int * 的指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,因此c++中使用 0 来表示 NULL。
#define NULL 0
但这种做法,在下面这种情况就会有二义性问题
void f(int a){
cout << a << endl;
};
void f(int* a){
cout << "*" << endl;
};
int main() {
f(NULL);
}
提示:
Call to ‘f’ is ambiguous 编译器不知道该调用哪个函数,因为两个都符合。
如果使用 nullptr就不会,它的类型为 nullptr_t ,一个模板类型的空指针,可以指向任何类型的对象。下面的函数会调用 f (int* a) 函数。
int main() {
f(nullptr);
}
12: 悬空指针
其实就是野指针的一种,指向的对象被delete,但是指针没有设置为nullptr,仍然指向那块内存。
13: 运行时类型识别 dynamic_cast
首先要说的是,c++中无论使用虚函数实现多态还是 dynamic_cast 都需要共有继承。
有如下继承关系:
class Base{
};
class Child : public Base{
};
下面的写法默认是对的
Base* b = new Child();
但这么写就是错的
Base* b = new Base();
Child* c = b; //错误,子类可能包含父类没有的方法或成员变量,因此这么做是不安全的。
如果一定要将一个派生类指针或引用转化为基类的指针或引用,就需要使用 dynamic_cast
Child* c = dynamic_cast<Child*>(b);
14: 编译过程
- 预处理 -> 主要就是宏定义替换,预处理命令,c++还有const类型的替换等,生成.i© .II(c++)
- 编译 -> 将c源码编译成汇编,这一步生成的文件后缀 .s
- 汇编 -> 编译成机器指令,这一步生成 .o
- 链接,将编译后的文件,彼此链接。
15: list的使用
c++中的双向链表,如果需要在线性adt中插入数据,使用list是比较合适的,特别是最近常遇见的围着一个桌子报数的问题。如果两端需要插入就用deque。
stack、queue、deque、vector 都支持随机访问,但list是不支持的,++ – 运算符在list中都被重载了,访问上一个或者下一个节点。
基本使用:
list<int> l = {1,2,3,4,5,6}; //使用{}的形式初始化。
auto p = find(l.begin(), l.end(), 3);
l.erase(p); //删除p指向的元素,但是erase的返回值指向下一个可用的迭代器。
p = find(l.begin(), l.end(), 4);
l.insert(p, 99); //向p之后插入一个元素。
for(auto i=l.begin(); i!=l.end(); i++){ //++运算符,在这里并不是地址加1的意思,而是向后挪了一个节点,--就是向前移了。
cout << *l << endl;
}
16: value_type
stl中每一种容器都有自己的value_type,使用迭代器取值的时候,值的类型就是value_type,例如
map<int,string> m;
m::value_type(); //这里的变量类型就是 pair<int,string>
17: vector、map 越界访问下标
使用迭代器和 [] 访问下标的时候,不会做边界检查。也就是说,即使越界了,也不会报错。如下面的代码
vector<int> v = {1,2,3};
v[4];
17: 三个内存分配函数 malloc、relloc、calloc
//分配内存,但不初始化
void* p = malloc(10);
//分配 n块大小为size的内存,并初始化为0
void* sp = calloc(10, sizeof(int));
//改变内存大小(扩大或缩小),如果原内存后面没有足后可以使用的内存,就会寻找一块新的内存,因此新内存的起始位置可能和原内存不同
//所以使用realloc扩容就要使用它的返回值
p = realloc(p, 20);
18:c++ cast汇总
c++中一共有四种cast,用于类型转换,分别是 static_cast、const_cast、dynamic_cast、reinpreter_cast
static_cast: 就是普通的强制类型转换,比如
void* p;
//下面两种方式是一个意思
char* c = (char*)p;
char* c2 = static_cast<char*>p
是一个意思,但不能将const转非const,也不能将基类指针或引用转换为派生类指针或引用,volitale,__unaligned 也不能用。
如果想将基类指针转换为派生类指针,就要使用 dynamic_cast,前面有说。
如果想将const类型的指针或引用转换为非const类型的指针或引用,就要用 const_cast
const char* c = "zifuchuan";
const int& i = 0;
char* c2 = const_cast<char *>(c);
int& i2 = const_cast<int&>(i);
reinpreter_cast 只能转换指针、引用(转的是引用的那个变量)、算数类型,转换的前提是所占内存相同
int val = 10;
int* a = &val;
long b = reinterpret_cast<long>(a);
这里64位操作系统,指针占8个字节,因此只能转为long,而不是int。
19: allocator、deallocator
- 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;
- 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8~128bytes的小额区块;
- 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;
- 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。
20: 函数参数管理 __stdcall、__cdcel、__fastcall
- __stdcall:
函数参数从右向左入栈,由被调用者清除栈内参数,windows api默认方式。 - __cdcel:
函数参数从右向左入栈,由调用者清除栈内参数,c/c++默认方式。 - __fastcall:
从左开始,小于4字节放入cpu寄存器中,其余参数由右向左入栈。
另外,三种参数,编译后,函数修饰的名字不同。
21: 智能指针和实现
c++中的智能指针一共有3个 shared_ptr、unique_ptr、weak_ptr(伴随类),其中shared_ptr和weak_ptr的使用,和oc中strong、weak类型指针很像,一个会让引用计数器+1,一个不会,但oc中的引用计数是编译器特性,shared_ptr则是用自己的构造函数和析构函数完成的。
shared_ptr:允许多个智能指针指向一个对象,每多一个指针,引用计数器+1,引用计数器为0时,释放对象。
Class Obj{};
shared_ptr<Obj> s = make_shared<Obj>(); //创建一个Obj对象,并使用智能指针进行管理。
shared_ptr<Obj> s2(s); //拷贝构造,s和s2指向同一个对象,因此引用计数为2。
Obj* p = new Obj;
shared_ptr<Obj> s(p); //也可以使用普通指针初始化智能指针
但是如下方式是错的, 因为智能指针
shared_ptr<Obj>s = p;
当然,一般方便使用,指针类型直接使用auto就可以了。
自己实现智能指针:
template<class T>
class Shared_ptr{
private:
unsigned int* _count;
T* _p;
public:
Shared_ptr():_count(new unsigned int(0)),_p(nullptr){};
Shared_ptr(T* p):_count(new unsigned int(1)),_p(p){};
Shared_ptr(Shared_ptr& other):_count(other._count),_p(other._p){};
~Shared_ptr(){
if (_p && --*_count==0) {
delete _count;
delete _p;
}
};
T& operator*(){
if (_count == 0) {
return nullptr;
}
return *_p;
};
T* operator->(){
if (_count == 0) {
return nullptr;
}
return _p;
};
//=运算符有返回值,主要是为了连续赋值
//other指向的内存多个一个指针,引用数+1
//this类指向发生了改变,因此原先计数器-1
Shared_ptr& operator=(Shared_ptr& other){
other._count++;
if (--_count == 0) {
delete _p;
delete _count;
}
this->_count = other._count;
this->_p = other._p;
return *this;
};
};
22: lambda函数的实现
以 [] 开头的就是 lambda函数,也就是匿名函数,不需要写返回值。
cout << [](int a,int b){ return a<b; } << endl;
23: hello world程序执行过程
- 通过可执行文件首部信息计算出代码和数据在可执行文件中的位置,并找到相应的磁盘块。
- 生成一个进程,将代码和数据映射到进程中(并不是读入)。
- 设置cpu上下文环境,并跳到程序开始处。
- 执行第一条指令,发生缺页码异常。
- 分配一页物理内存,将代码读入内存中。
- 执行puts函数,在显示器上写一串字符串。
- 内核调用显示进程,进程调用输出设备输出字符串。
24: explicit 禁止隐式类型转换
先看一下隐式类型转换
class Obj{
public:
Obj(int val){};
};
int main() {
Obj o = 10; //会直接调用一个参数的构造函数
}
使用explicit 禁止掉隐式类型转换
class Obj{
public;
explicit Obj(int val){};
}
25: 如果在类的成员函数中调用delete this?
- c++的对象模型只包含数据成员和指向虚函数表的指针 vptr,成员函数在代码段中,由所有对象公用。
- 为了知道是那个对象调用了成员函数,在调用成员函数的时候,会传递一个this指针,指向调用对象。
- delete this后,成员函数只要不涉及对象内容,仍然可以正常运行,如果设计对象内容,由于对象已经被释放,就会出现不可预期的问题,因为要调用的内存已经被回收了,this变成了野指针。
26: 动态联编和静态联编
- 静态联编:在编译阶段确定调用和代码的映射关系,优点是效率比较高。
- 动态联编:在程序运行时,确定调用和代码的映射关系,优点是比较灵活,缺点是效率较低。
c++中,动态联编是通过虚函数实现的。
27: 静态编译和静态编译
主要针对程序中使用的动态连接库
静态编译: 将动态链接库中需要的部分,提取出来,连接到可执行文件中,优点是可执行文件执行时不需要依赖动态链接库,缺点是编译速度慢,可执行文件体积大。
动态编译: 可执行文件附带一个动态连接库,需要执行时,直接调用动态连接库中的命令。优点是编译速度快,缺点是缺少动态连接库,程序就无法运行,因此需要附带庞大的动态连接库。
28: 动态连接库和静态连接库
但如果是动态编译,动态连接库只会在需要的时候,调用,而静态连接库会被包含到可执行文件中。
如果是静态编译,动态链接库也只会包含需要的部分。
动态连接库可以包含静态连接库,但反过来就不行。
29: 经典算法,不使用额外空间交换两个数?
这道题考的比较少了,但还是写一下吧。
a = a + b;
b = a - b;
a = a - b;
30: main(int argc, char* argv[])
main里的参数主要用于接收程序启动时的命令参数,其中argc是参数的个数,argv是一个char**的二纬数组,里面就是参数,第一个参数是应用程序的名称。
之前面试时问过一个经典问题,iOS在外部使用scheme跳转到应用程序内的时候,如果这时候程序没有启动,如何知道要跳转到那个页面呢?
跳转页面就是以启动参数的形式传递进来的,在argv里。
31: struct、union
c和c++中struct的区别主要就是多了封装和方法了,c++中,struct除了默认访问权限是public,和class没有什么区别。
c中的union比较简单,主要就是一种类型复合,内存占用是其中最长的成员变量的数值。
union u{
int a;
double c;
char c[];
}
但c++中的union就比较神奇了,可以添加方法和权限,默认访问权限是public,但不能派生和继承。
union u{
int a;
double c;
void say(){
cout << c << endl;
};
}
union有一个用途就是用来测试大端序和小端序。
union test{
char b;
short a;
};
int main() {
test t;
t.a = 0x1234;
if (t.b == 0x12) { //高位在内存第位
cout << "big" << endl;
}else if(t.b == 0x34){ //低位在内存低位
cout << "small" << endl;
};
}
32: volatile关键字的作用
使用这个关键字修饰的变量,编译器对访问该变量的代码就不再进行优化,可以提供对特殊地址的稳定访问。
一般来说,下面几种情况需要加上violate
- 多任务下,各任务之间的共享标志应该加volatile
- 中断服务中修改供其他程序检测的变量
- 存储映射的寄存器
33: new、operator new、placement new、operator delete、delete
new:创建并初始化对象
operator new:分配内存,但不初始化对象
placement new:初始化对象
因此 new = operator new + placement new
delete:删除对象,并置空指针
operator delete:类似free
34: 什么时候会有合成构造函数、合成析构函数?
四种情况
1: 父类有构造函数
2: 成员对象有构造函数
3: 有虚函数
4: 有虚基类
35:何时必须使用初始化成员列表?
1: 成员变量为引用类型的时候
2: 成员变量为const类型的时候
36: assert 和 NDEBUG
assert定义在assert.h中,如果它的条件返回错误,则终止程序执行。
#include "assert.h"
void assert(int expression);
NDEBUG 如果定义了,assert就不会被执行。
37:strcpy和strnpy
先说结论:strnpy更安全,排除了strcpy可能会遇到的内存重叠和目标串空间不足的问题。
strcpy(char *des, const char *src);
strnpy(char *des, const char *src, int pos); //复制pos长度的字符,如果字符重叠或者空间不够就会报错。
38:c++中新参命名问题
c++中形参在是可以不命名的,这一点一般用在声明的时候,比如:
int f(int, float)
当然,实现的时候,也可以不给形参命名,但就没有什么意义了,因为无法使用传入的参数。
int f(int, float){}
如果将这种写法用于函数指针上,就可以写出如下的代码,接收参数是一个函数指针,这个指针指向一个传入值为空,但返回值为int的变量。
int f(int());
这种写法用于某些对象时,就会出现二义性问题,比如下面的原函数调用,第一眼看上去是调用了函数f,接收参数是一个Obj对象(调用了默认构造函数生成的临时变量)。
但实际上,编译器会理解为,我们传入了一个函数指针,这个指针的返回值是Obj对象,传入值为空。
class Obj{
}
int f(Obj());
在c++多线程编程时,调用函数对象也很容易出现类似的问题
class Task{
void operator()() const{
cout << "do something" << endl;
}
}
//这里想要表达的意思,是使用默认构造函数生成了一个函数对象,然后调用拷贝构造传进去,但是编译器会理解为传入了一个函数指针,指向一个返回值为Task但传入值为空的函数。
thread t(Task());
如果要消除这种二义性,可以在外面加一组小括号
thread t((Task()));
更好的是使用新的统一的初始化语法:
thread t{Task()};