面试问题-c++基础
一些c++基础问题
关键字
static关键字的作用
如果static修饰的是一个全局变量,那么该变量转化为全局静态变量,只有包含它的文件可见,可以实现隐藏
如果static修饰的是一个成员变量,那么该变量堆此类的所有对象共有,而且只分配了一次内存,每个对象初始化时不会再次对该静态变量重新初始化
如果static修饰的时一个成员函数,和静态成员变量一样,所有类对象共享,但是静态成员函数不依赖类对象,不需要实际初始化一个对象来调用静态成员函数,而且静态成员函数只能访问静态成员变量或者函数,如下面是一个例子:
#include <iostream>
using namespace std;
class Student{
private:
char *name;
int age;
float score;
static int num; //学生人数
static float total; //总分
public:
Student(char *, int, float);
void say();
static float getAverage(); //静态成员函数,用来获得平均成绩
};
int Student::num = 0;
float Student::total = 0;
Student::Student(char *name, int age, float score)
{
this->name = name;
this->age = age;
this->score = score;
num++;
total += score;
}
void Student::say()
{
cout<<name<<"的年龄是 "<<age<<",成绩是 "<<score<<"(当前共"<<num<<"名学生)"<<endl;
}
float Student::getAverage()
{
return total / num;
}
int main()
{
(new Student("小明", 15, 90))->say();
(new Student("李磊", 16, 80))->say();
(new Student("张华", 16, 99))->say();
(new Student("王康", 14, 60))->say();
cout<<"平均成绩为 "<<Student::getAverage()<<endl;
return 0;
}
运行结果:
小明的年龄是 15,成绩是 90(当前共1名学生)
李磊的年龄是 16,成绩是 80(当前共2名学生)
张华的年龄是 16,成绩是 99(当前共3名学生)
王康的年龄是 14,成绩是 60(当前共4名学生)
平均成绩为 82.25
virtual关键字的作用
多态的实现,加了virtual的函数为虚拟函数,会根据多态性决定调用基类还是派生类函数,没有virtual会调用基类的方法
const关键字
限定一个变量为只读
当修饰常量,常量值不可修改
const int i=4;
i=5; //无效
当修饰指针
int a = 9;
int b = 10;
const int* p = &a;//p是一个指向int类型的const值,与int const *p等价
*p = 11; //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b; //合法,改变了p的指向
当修饰引用
int a = 9;
int b = 10;
int* const p = &a;//p既是一个const指针,同时也指向了int类型的const值
*p = 11; //编译正确,p不可以修改,但是可以通过*p修改
p = &b; //编译错误,p是一个const指针,只读,不可变
其实主要是看const后面的变量是什么,只有const后面的变量无法修改
extern关键字
在本文件使用extern声明,可以使用在其他文件定义的变量或者函数
explicit关键字
修饰类的构造函数
防止隐形转化的发生,如下面三种类的声明和初始化发生隐式转换
class Circle
{
public:
Circle(double r) : R(r) {}
Circle(int x, int y = 0) : X(x), Y(y) {}
Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}
private:
double R;
int X;
int Y;
};
int _tmain(int argc, _TCHAR* argv[])
{
//发生隐式类型转换
//编译器会将它变成如下代码
//tmp = Circle(1.23)
//Circle A(tmp);
//tmp.~Circle();
Circle A = 1.23;
//注意是int型的,调用的是Circle(int x, int y = 0)
//它虽然有2个参数,但后一个有默认值,任然能发生隐式转换
Circle B = 123;
//这个算隐式调用了拷贝构造函数
Circle C = A;
return 0;
}
//添加explicit
class Circle
{
public:
explicit Circle(double r) : R(r) {}
explicit Circle(int x, int y = 0) : X(x), Y(y) {}
explicit Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}
private:
double R;
int X;
int Y;
};
int _tmain(int argc, _TCHAR* argv[])
{
Circle A = 1.23; //编译错误
Circle B = 123;//编译错误
Circle C = A;//编译错误
return 0;
}
override关键字
override作为成员函数的修饰关键字,主要作用是避免一些继承的错误,比如,我们看下面这个例子
class A
{
public:
virtual void func1() const {};
virtual void func2(int = 0) {};
virtual void func3() {};
};
class B :public A
{
public:
virtual void func1() {}; //编译不会出错,会把当前函数作为一个新的成员函数,而不是继承,继承失败
virtual void func2(double = 0.0) {}; //编译不会出错,但是参数不同,会作为新的成员函数
virtual void func4() {}; //编译不会出错,根本就是个新的成员函数,可能是你手抖写错函数名
};
//但是如果你把class B写成下面的形式,就不能通过编译,可以及时发现问题
class B :public A
{
public:
virtual void func1() override { cout << "B::func1" <<endl; }; //编译不会出错,会把当前函数作为一个新的成员函数,而不是重写
virtual void func2(double = 0.0) override { cout << "B::func2" <<endl; }; //编译不会出错,但是参数不同,会作为新的成员函数
virtual void func4() overide { cout << "B::func3" <<endl; }; //编译不会出错,根本就是个新的成员函数,可能是你手抖写错函数名
};
智能指针
在堆上分配的内存
STL中一般有四种智能指针:
- unique_ptr(独享指针),一个独享指针只能占领并管理一个对象;
- shared_ptr(共享指针),多个共享指针可以指向一个对象;
- weak_ptr(弱指针),这是用来辅助shared_ptr的指针,当shared_ptr管理的对象形成环时,这时候其中一个对象使用weak_ptr可以打破环;
- auto_ptr被c++11弃用;
指针和引用的区别
指针有着自己分配的空间,引用只是对象的别名,大小为被引用对象的大小
指针和引用做参数的区别
- 两者虽然都可在函数内改变实参值,但是引用毕竟是实参的别名,不占用内存,指针传入的是实参的地址
- 并且改变指针不能改变实参地址,改变引用可以改变实参的地址,因为指针做形参传递的是一个拷贝,而形参传递的是一个实参的地址
虚函数表
C++实现虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表(virtual function table, vtbl)
虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址(可以理解为数组里存放着指向每个虚函数的指针)
子类重写父类函数时,实际改变了虚函数表里对应地址
map和set的区别
map是key-value的形式,set只有value,key就是value,所有set不允许有重复值,而map允许有重复值
map与unordered_map和hash_map和multimap的区别
map和multimap底层是红黑数,而unordered_map和hash_map底层是哈希表,红黑书是有序的,哈希表是无序的,而multimap可以具有相同的键值,map键值唯一,一般推荐unordered代替hash_map
strucu和class的区别
在C++中,可以用struct和class定义类,都可以继承。区别在于:struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
const和#define有什么区别
const有类型,define没有类型
数组与指针的区别
- 同类型指针可以相互赋值,数组不行,需要使用下标
- 数组开辟连续的内存,大小由元素个数决定,指针的大小64位为8个字节,32位为4个字节
- 指针可以寻址,++,–,数组名不行
空类默认有哪些成员函数,类的大小
构造函数、析构函数、拷贝构造、赋值运算符、取址运算符和一个this指针
空类只占一个字节,虚指针4个字节,函数不占字节,继承会继承基类的大小,int占4个字节
class A {}; //一个字节
class B
{ //一个字节
void fun(){};
}
class C
{ //4个字节
virtual void fun(){};
}
class D
{ //8个字节
virtual void fun(){};
int i=0;
}
class E:public D //8个字节
{
}
class E:public D //12个字节
{
virtual void fun(){};
}
子类继承父类,子类的构造和析构顺序,为什么父类的析构函数需要virtual关键字
子类实例化时先调用父类的构造函数,接下来才是子类的构造函数,析构相反,先是子类,再是父类;如果父类析构不是虚函数,当我们删除父类的指针时,只会调用父类的析构函数,不会调用子类的析构函数,导致父类被删除,子类还存在的现象。
说说公有继承、受保护继承、私有继承和共有成员、受保护成员、私有成员
公有继承时,派生类对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有和受保护成员,基类的公用成员和保护成员在派生类中保持原有的访问属性;
私有继承时,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问员;
受保护继承时,基类的公有成员和保护成员都作为派生类的受保护成员,并且不能被这个派生类的子类所访问,所有基类成员均变成派生类的私有成员。
一个类的公有成员在任何地方都可以被访问
一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问
C++如何阻止一个类被实例化
将构造函数设为private
c++ main函数执行前后
main函数执行前,全局变量和对象的构建,初始化,不接受赋值操作;main函数执行;main函数执行完毕后,回收全局变量和对象,调用析构函数
请描述进程和线程的区别
- 进程是系统资源调度分配的一个独立单位;线程是CPU调度和分配的基本单位
- 线程运行在线程内,进程是线程的容器,一个进程可以有很多的线程
- 进程有独立的内存,同一进程的线程间共享内存,所以进程的切换开销大,线程切换的开销小
new/delete与malloc/free的区别是什么
1、属性:new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持
2、返回:new返回的是指定对象的指针,而malloc返回的是void*
3、参数/内存:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
4、分配失败:new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
5、内存区域:new分配内存不一定在堆区;malloc分配内存在堆区
6、new会调用构造函数
内存
请说说c++的内存分配
- 栈区(局部变量区):编译器自动编译释放,存放函数参数,局部变量等
- 堆区(动态存储区):一般由程序员分配释放,存放的是new/malloc分配的内存块,delete/free释放不过new操作索然一般在堆上分配,但也有在栈上分配的,new的内存分配区抽象来说应该是自由存储区
- 全局/静态区:存放全局和静态变量
- 文字常量区:存放字符常量等只读数据
- 程序代码区:存放函数体的二进制代码
静态内存分配和动态内存分配
静态内存分配发生在栈区,由系统实现,自动释放,比如一个数组
int p[100];
动态内存分配则是在堆上实现,编译器会自动计算,需要手动释放,一般是new关键字和malloc库函数,一样是定义一个一维数组,分别使用malloc和free
#include <iostream>
#include<stdlib.h> //该头文件为malloc必须
using namespace std;
//使用malloc/free动态分配一维数组
int main()
{
int len;
int *p;
cout<<"请输入开辟动态数组的长度:"<<endl;
cin>>len;
//长度乘以int的正常大小,才是动态开辟的大小
p = (int*)malloc(len*sizeof(int));
cout<<"请逐个输入动态数组成员:"<<endl;
for(int i=0; i<len; ++i)
{
//此处不可以写成:cin>>*p[i]
cin>>p[i];
}
cout<<"您输入的动态数组为:"<<endl;
for(int i=0; i<len; ++i)
{
cout<<p[i]<<" ";
}
//时刻记住:有malloc就要有free
free(p);
}
//使用new/delete
int main()
{
int len;
cout<<"请输入开辟动态数组的长度:"<<endl;
cin>>len;
//长度乘以int的正常大小,才是动态开辟的大小
int* p=new int[len];
//数据输入
cout<<"请逐个输入数据:"<<endl;
for(int i=0; i<len; ++i)
{
cin>>p[i];
}
//数据反馈
cout<<"您分配的动态数组为:"<<endl;
for(int i=0; i<len; ++i)
{
cout<<p[i]<<" ";
}
//释放内存:
delete []p;
}
堆和栈的区别
- 堆存放的是动态分配的内存,栈一般存放一些静态分配的内存,如局部变量和参数等
- 堆的地址是从低到高,栈的地址从高到低,所以栈相对有限制,堆更灵活
- 栈由系统自动分配和释放,所以速度快;堆需要手动回收,速度较慢,且容易产生碎片
内存泄露
内存泄漏是指分配了内存却没有回收导致的错误,常见的原因有以下几点:
- new或者malloc没有及时delete或free
- new []和delete []没有匹配
- delete void*指针
- 基类的析构函数没有vurtual,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数
内存溢出
程序在申请内存时,没有足够的内存空间供其使用,内存泄漏最终会导致内存溢出
段错误
访问了受保护或者不存在的内存,具体原因有如下:
- 数组越界,数据类型不一致
- 有些内存是内核占用的或者是其他程序正在使用,比如操作一个文件时,另外一个程序也想要操作这个文件
- 野指针或者是指针没有初始化,默认为NULL
野指针
- 没有初始化的指针,默认为NULL
- 指针被free或者delete之后,没有置为NULL
浅copy和深copy和赋值(深拷贝和浅拷贝)
浅copy只是复制了对象的地址,即指针,和原对象共享同一地址,改变其中一个的值,另一个也会跟着改变;
深copy是将整个对象复制,有独立的地址,两个对象相互独立
赋值和浅copy的区别:赋值是复制了栈中的地址,不论赋值的是基本数据类型还是地址,新旧对象具有关联;浅copy如果复制的是基本的数据类型,那么改变其中一个,另一个不会改变,如果复制的是地址,如数组等,改变其中一个,另一个会发生变化
显式类型转换
- const_cast
一般用来区常量化,去const
const int* p;
int *q=const_cast<int*> (p);
- static_cast
一个是基本的数据类型之间的转换如int和char
一个是非多态下,子类转基类,基类转子类,多态下的基类转自类不安全,会退出
class A
{
public:
void fun() {};
};
class B : public A
{
};
class C : public A
{
public:
virtual void fun() {};
void fun2() {};
};
int main()
{
int n = 9;
char s = static_cast<char>(n); //基本的数据类型转换
A *a = new A;
B *b = new B;
//C *c = new C;
//D *d = new D;
a = static_cast<A*>(b); //上行,子类转基类安全
b = static_cast<B*>(a); //下行,非多态下基类转子类安全
C *c = static_cast<C*>(a); //下行,多态下,不安全,但是没出错
}
- dynamic_cast
和static_cast类似,不过上行下行都是安全的,下行失败返回null,而不是退出
- reinterpret_cast
强制类型转换符用来处理无关类型转换的
int *ptr = new int(233);
uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
一个C++源文件从文本到可执行文件经历的过程
1)预编译,预编译的时候做一些简单的文本替换,比如宏替换,而不进行语法的检查;
2)编译,在编译阶段,编译器将检查一些语法错误,但是,如果使用的函数事先没有定义这种情况,不再这一阶段检查,编译后C/C++代码变为汇编代码,得到.s文件
3)汇编,汇编代码变为机器码,得到.o或者.obj文件
4)链接,将所用到的外部文件链接在一起,在这一阶段,就会检查使用的函数有没有定义,链接过后,形成可执行文件.exe
移动拷贝函数、移动赋值函数和拷贝构造函数,拷贝赋值函数
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A构造" << endl; };
A(char* p) :name(p){ cout << "A带参构造" << endl; };
~A() { cout << "A析构" << endl; };
A(A& t)//拷贝构造函数
{
cout << "拷贝构造函数" << endl;
if (t.name) this->name = t.name;
//或者深拷贝
if (t.name)
{
int lenth = strlen(t.name);
this->name = new char[lenth + 1];
strcpy(this->name, t.name);
}
}
A& operator=(A& t) //拷贝赋值函数
{
cout << "拷贝赋值函数" << endl;
if (this != &t)
{
delete name;//删除原有内存
if (t.name)
{
int lenth = strlen(t.name);
this->name = new char[lenth + 1];
strcpy(this->name, t.name);
}
}
return *this;
}
A(A &&t) //移动构造函数
{
cout << "移动构造函数" << endl;
if (t.name)
{
this->name = t.name;
t.name = nullptr; //t析构更安全
}
}
A* operator=(A &&t) //移动赋值函数
{
cout << "移动赋值函数" << endl;
if (t.name)
{
delete name;
this->name = t.name;
t.name = nullptr; //t析构更安全
}
return this;
}
private:
char *name;
};
int main() {
char *p="ok";
A a=A(p); //1
A b; //2
b = a; //3
A c = A(a); //4
A d = move(c); //5
d = move(b); /6
}
1.带参构造
2.A构造
3.拷贝赋值函数
4.拷贝构造函数
5.移动构造函数
6.移动赋值函数
A析构
A析构
A析构
A析构
c++成员初始化列表为什么会性能更好
对于一般的数据类型,例如int, float, char等在初始化列表和在构造函数内两者的区别并不是很大,但是如果是自定义的类成员,区别就大了,使用初始化列表调用的是拷贝构造函数,而在构造函数中调用的类成员的构造函数和赋值函数,所以在构造函数中需要进行两步,性能也更慢一点
不同情况下类的大小
主要遵循一下几个原则:
- 类的静态成员变量不占类的空间
- 空类的大小是1个字节
- 类不为空的情况下,不计这1个字节大小
- 类的虚函数占4个字节
- 类的普通成员函数不占空间
- 类继承需要把基类大小加上
- string占用28个字节
class A1
{
virtual void fun() {};
};
class A2
{
virtual void fun() {};
virtual void fun1() {};
};
class B1 {
int b;
};
class B2: private A1
{
int b;
};
class C1{
void fun() {};
};
class C2
{
int a;
void func() { cout << "ok"; };
};
class D1
{
static int a;
};
class D2
{
static int a;
int b;
};
class E
{
string a="ab";
};
int main()
{
cout << sizeof(A1)<<endl; //4
cout << sizeof(A2) << endl; //4
cout << sizeof(B1) << endl; //4
cout << sizeof(B2) << endl; //8
cout << sizeof(C1) << endl; //1
cout << sizeof(C2) << endl; //4
cout << sizeof(D1) << endl; //1
cout << sizeof(D2) << endl; //4
cout << sizeof(E) << endl; //28
}
成员列表和构造函数的执行顺序
成员的初始化顺序与成员的声明顺序相同
成员的初始化顺序与初始化列表中的位置无关
初始化列表先于构造函数的函数体执行
什么情况必须使用成员列表初始化
- 初始化cost成员变量时
- 初始化引用成员变量时
- 初始化的自定义的类数据,并且类数据不包含构造函数
内存对齐
按多少字节对齐,首先取alig=Min(所有成员中最大的, 默认对齐方式),默认一般是4个字节
然后每个成员大小是alig的整数倍,并且最后计算的总和也是alig的整数倍
内联函数和宏定义区别
内联函数和宏定义都是在调用的时候进行展开,但是内联函数相对于宏定义有一些优点:
宏定义相对来说不那么安全,例如在某些场景替换会导致符号优先级错误,得不到意想的结果,这叫做宏的边界效应
内联函数支持调试,宏不支持
宏无法访问类的私有成员函数,而内联函数可以自由访问
另外,只有当函数只有 10 行甚至更少时才将其定义为内联函数