0%

面经C++基础知识1

面经 C++ 基础知识

C++基础知识

static 关键字的作用

  1. 修饰变量

    • 局部静态变量:存放在静态存储区,不随函数的退出而销毁,默认初始化为 0
    • 全局静态变量:作用域是 定义该变量位置开始到文件结尾,而常规的全局变量其他cpp文件可以通过 extern引用
  2. 修饰函数

    ​ 加上static 修饰函数之后,该函数只能被当前文件调用,不会被外部同名函数影响。

  3. 类中使用

    • 类中的静态变量:该类实例化的所有对象的静态变量 公用一个存储区,即公用一个变量,使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。
    • 类中的静态函数:同样也是所有对象公用的函数,成员调用函数不需要 加上对象引用,直接调用。静态函数中可以直接调用类中申明的静态函数,但是想要调用 类中非静态函数,需要 以 <类名>::<静态成员函数名>(<参数表>) 的格式。

怎么理解面向对象

和面向过程一样,面向对象也是一种编程思想。面向过程是将问题一个复杂的问题分解为多个子过程逐步解决。理论上任何复杂的系统都可以用面向过程的方法解决。但是随着系统的复杂,各个过程之间一定存在相似性和耦合性,面向过程的方法效率就很低。而面向对象的思想更加适合处理一个系统性的问题,着眼于整个面,总结同类事物的共性。使用多态的方法定义和其他类别对象沟通的统一接口;使用封装的特性将数据 和 方法封装为一个整体,简化外部调用的复杂度,并保护内部的私有数据;使用继承的方法实现同类事物的代码复用和特性扩展。

C/C++区别

  • 设计思想上 C 是面向过程的结构化编程语言,而C++是面向对象的语言,因此C++具有 额外的 封装,继承,多态三大特征

  • C++ 增加类型安全的操作,例如 强制类型转换

  • C++ 支持范式编程,例如 模板类 和 函数模板

C++四种cast转换的区别

const_cast<T>(expression);

  • reinterpret_cast 重解释转换 : 和 c 语言的强制转换中的部分功能—数据的二进制形式重新解释,但是不改变其值 。 如指针转int这种,灵活度高。
  • const_cast : 用于去掉/增加指针/引用的 const 属性 (分为两种,一个是全局的const,它在编译时会被放在只读数据区,通过地址尝试修改只读数据区会产生运行错误。而函数局部const变量 存在栈上,可以通过指针修改变量的值)
  • static_cast :
    1. 和 c 语言中的强制类型转换 的功能一样 如 float -> int 但是这种会对数据做二进制的修改(丢失小数点的精度),这点 和 重解释转换不一样。
    2. 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换,可以上行转换,下行转换由于没有动态类型检查,运行会异常。
    3. 不能去掉 变量const, volite关键字
  • dynamic_cast : 类层次间类指针或引用的转换,运行时会检查转换的可行性,这点与static 不同的是,下行转换 会返回 null (通过type_info 来判断)因此更安全。(指针空 引用异常)

C++指针和引用的区别

  1. 指针是一个变量,占内存,而引用是一个变量的别称,不占内存。

    • sizeof(指针) 和 sizeof(引用) 不同
    • 指针有const 而 引用没有
  2. 指针可以更改指向的对象,而引用一旦确定,就和所引用的对象绑定了,不能更改

  3. 指针需要 被解引用 才能对对象操作,而引用直接就是对对象的操作

  4. 指针可以多级 引用只能一级。

  5. 如果返回动态分配的内存 只能用指针。

关于引用

左值引用和右值引用…..

引用不能 绑定 临时变量,右值,例如常量就是一个右值。只有const 引用可以同时接收 左值 和 临时右值。右值说白了就是给零时变量起了个新名字 从而延续了临时变量的生命周期。

右值引用 语义移动,move 可以将左值强制转换为一个右值,原理就是使用static_cast 将左值强制转化为右值引用,而完美转发的原理是 将 根据模板去推断 static_cast<T&&>的类型,原理是引用折叠。

关于运算符的重载

分为类内重载 和 全局范围重载(并不是所有运算符都能重载 也不是所有运算符都能全局重载 ->)

最基本的重载运算符不能改变原有运算符的优先级 用法,操作数数量(在类中重载二元操作数数量少一个 因为类自身算法一个)…

全局范围重载 通常需要访问操作对象的私有变量,所以想要全局重载则要使用友元函数。同时在全局范围重载的运算符形参必须有一个是自定义类,这是为了防止程序员修改操作符原本的属性例如 int 变量的加法改为减法。

C++智能指针

auto_ptr< > , unique_ptr< > , share_ptr< >, weak_ptr< > 。当 new 了 一个指针后,必须手动delet,否则会内存泄漏,为了解决这个缺点,c++中增加了 智能指针,智能指针实际上是一个类,对new出来的指针用 类 进行了一层封装,超出类的作用域的时候会自动调用析构函数释放指针指向的内存,并在类中 对 () -> 等操作符进行了重载。智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏

  1. auto_ptr : 是 c++98里的东西,在 c++11已经被弃用,但是后面三个的基本用法还是类似的。
1
2
3
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错. 这时 p2 剥夺了 p1 的所有权,但是调用 p1 的时候还是会内存崩溃,因为此时p1指向的其实是空地址
  1. unique_ptr : 独占的意思,就是某个指针,只能被一个智能指针独占,当试图使用赋值运算符去复制指针时,编译器会报错,解决了auto_ptr中潜在的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4; //#5
p4 = p3; //此时编译会报错!!
//但是 如果是临时右值 可以赋值,因为 临时右值执行完之后就被析构了 不存在风险
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许

//但是想要重用这个指针 可以使用 便准库中带的 move
unique_ptr<Test> ptest(new Test("123"));
unique_ptr<Test> ptest2(new Test("456"));
ptest->print();
ptest2 = std::move(ptest);//不能直接ptest2 = ptest
if(ptest == NULL) cout<<"ptest = NULL\n";
  1. share_ptr : 上面的 unique_ptr虽然很安全,但是不方便,不能赋值要求太死了。share_ptr 使用计数机制来表明资源被几个指针共享。当计数值为0 就释放资源。调用 reset 放弃内部对象,会使引用对象计数值减一。
1
2
3
4
5
6
7
8
9
10
11
shared_ptr<Test> ptest(new Test("123"));
shared_ptr<Test> ptest2(new Test("456"));
cout<<ptest2->getStr()<<endl;
cout<<ptest2.use_count()<<endl;
ptest = ptest2;//"456"引用次数加1,“123”销毁
ptest->print();
cout<<ptest2.use_count()<<endl;//2
cout<<ptest.use_count()<<endl;//2
ptest.reset();
ptest2.reset();//此时“456”销毁
cout<<"done !\n";
  1. weak_ptr : 上述的 共享指针在相互引用时 会出现 锁死的情况,导致内存泄漏,就是 计数值永远减不到0的情况。而weak_ptr 指针就可以弥补这个缺点。weak_ptr 指向一个 shared_ptr 管理的对象,它不控制对象的生命周期,进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。

给定三角形三顶点 判断点是否在三角形里

ABC 即判断 面积 OAB + OAC + OBC = ABC 是否成立即可。计算面积使用海伦公式 S = ( x1 y1 1 x2 y2 1 x3 y3 1 ) 构成的3x3行列式的值的一般即为 面积。

C++多态

多态性可以简单地概括为 “一个接口,多种方法”,它是面向对象编程领域的核心概念。

多态的目的:封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了“接口重用”。也即,不论传递过来的究竟是类的哪个对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

c++多态的两种形式:

  1. 编译时多态:通过函数重载,函数模板 实现

  2. 运行时多态性(动态多态):通过虚函数实现,重写 (一般函数子类和基类函数同名时对基类函数屏蔽的作用不叫 多态)

虚函数是实现多态的机制。其核心理念就是通过基类访问派生类定义的函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。

虚函数实现的过程是:通过对象内存中的虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数指针找到对应虚函数的实现区域并进行调用

  • 虚函数: 在类成员方法的声明(不是定义)语句前加“virtual”, 如 virtual void func()。子类可以不重写虚函数。子类如果不提供虚函数的实现,将会自动调用基类的缺省虚函数实现,作为备选方案;
  • 纯虚函数,在虚函数后加“=0”,如 virtual void func()=0。子类必须对纯虚函数实现,否则编译报错。(纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路)

为什么C++默认的析构函数是不是虚函数,为什么析构函数最好是虚函数

为什么要是 虚函数:因为当使用基类指针指向 new 出来的子类对象时,如果子类中分配了新的内存,此时如果析构函数不是虚函数,会调用基类的析构函数 无法释放子类新定义的变量造成内存泄漏。 为什么c++默认不是虚函数:当不使用基类指针指向子类对象时,就不会出现上述内存泄漏的情况,而定义虚函数 会多分配一个虚函数指针,占内存。

关键字volatile

表示一个变量也许会被后台程序改变,关键字 volatile 是与 const 绝对对立的。它指示一个变量也许会被某种方式修改,这种方式按照正常程序流程分析是无法预知的(例如,一个变量也许会被一个中断服务程序所修改

变量如果加了 volatile 修饰,则会从内存重新装载内容,而不是直接从寄存器拷贝内容。 volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile应用比较多的场合,在中断服务程序和cpu相关寄存器的定义。

当某个线程只负责读,某个线程只负责写时 volatile 修饰的变量是线程安全的

简述函数指针

函数指针就是一个指针变量,只不过它指向的是函数的入口地址,可以使用定义的函数指针去调用它指向的函数。概念上和指向变量的指针相似。 作用:可以用函数指针做回调函数 或者 函数的入口参数。

1
2
3
4
5
6
char *fun(char *p){  }
char * (*pfun)(char *); //函数指针
typedef char* (*pfun_ty)(char*);

pfun_ty pfun2 = fun;
pfun = fun;

c++中析构函数的作用

析构函数与构造函数对应,当其生命周期结束的时候,系统会自动调用析构函数,用户在类中使用了动态内存分配,就需要在析构函数中释放内存。如果用户没有显示的写析构函数,系统编译会自动生成一个析构函数。 顺序:子类本身的析构函数,子类对象成员的析构函数,基类的析构函数。

重载和覆盖的区别

重载:是相同的函数名,不同的参数列表(返回值没要求),程序编译时会根据调用的参数的类型选择对应的函数。编译时多态 重写: 子类继承父类,子类对父类的虚函数重写,在运行过程中确定是调用子类还是父类的函数。

请你说一说strcpy和strlen

1
2
3
char *strcpy(char* dest, const char *src);  //strcpy是字符串拷贝函数
//从src逐字节拷贝到dest,直到遇到'\0'结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。
//strlen函数是计算字符串长度的函数,返回从开始到'\0'之间的字符个数。

请你说一说你理解的虚函数和多态

多态:多态分为静态多态和动态多态,前者是重载实现,在编译的时候就确定了,而后者主要通过虚函数实现,是程序运行过程中动态确定要执行那个函数。例如 父类中含有一个虚函数,子类中重写了虚函数,当new一个子类对象并使用基类指针指向它时,此时指针调用虚函数调用的其实是子类中重写的函数,这就实现了一个接口复用的功能。 虚函数的实现:虚函数通过虚函数指针和虚函数表实现。当类种某个函数申明为虚函数的时候,这个类会多一个看不见的 虚函数指针,这个指针指向虚函数表,表中放了虚函数的地址(实际虚函数在代码段)。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

++i 和 i++ 的 区别, 以及他们的实现

记住 i++ 重载要使用 int 占位;而 ++i 不用;再根据作用 i++ 是先返回再自增,就是需要备份再自增,然后返回备份的值。

1
2
3
4
5
6
7
8
9
10
int int::operate++(int){   // i++  先返回 再自增  使用一个int占位 和 i++ 区分
int tmp = (*this);
(*this)++;
return tmp;
}

int int::operate++(){ // ++i 先自增 再返回
(*this)++;
return *this;
}

请你来写个函数在main函数执行前先运行

在main函数之前运行的 有以下几个思路:

  1. 全局变量在 main 开始之前就初始化了 ,因此可以定义一个全局 的 类 在 这个类的构造函数 和析构函数即为 main 之前之后调用的
  2. attribute 关键字
1
2
3
__attribute((constructor)) void before(){
cout << " start " << endl;
}

智能指针shared_ptr的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//  关键点 :
// 1. tmplate <typename T> 模板
// 2. 需要管理的指针变量设置为 T* ptr, 计数值也设置为 int * 指针,在构造函数时给 计数变量分配 内存并初始化为1
// 3. 除了构造函数中重新分配内存外,重载的 = 运算符 和 拷贝构造函数 都是公用计数变量 和 公用 管理的区域
// 4. 在拷贝构造函数中,需要先释放等号左值管理的内存区,再将其指针(计数指针和管理的指针)指向等号右边智能指针管理的地方
template <typename T>
class SmartPointer {
public:
//构造函数
SmartPointer(T* p=0): _ptr(p), _reference_count(new size_t){
if(p)
*_reference_count = 1;
else
*_reference_count = 0;
}
//拷贝构造函数
SmartPointer(const SmartPointer& src) {
if(this!=&src) {
_ptr = src._ptr;
_reference_count = src._reference_count;
(*_reference_count)++;
}
}
//重载赋值操作符
SmartPointer& operator=(const SmartPointer& src) {
if(_ptr==src._ptr) {
return *this;
}
releaseCount();
_ptr = src._ptr;
_reference_count = src._reference_count;
(*_reference_count)++;
return *this;
}

//重载操作符
T& operator*() {
if(ptr) {
return *_ptr;
}
//throw exception
}
//重载操作符
T* operator->() {
if(ptr) {
return _ptr;
}
//throw exception
}
//析构函数
~SmartPointer() {
if (--(*_reference_count) == 0) {
delete _ptr;
delete _reference_count;
}
}
private:
T *_ptr;
size_t *_reference_count;
void releaseCount() {
if(_ptr) {
(*_reference_count)--;
if((*_reference_count)==0) {
delete _ptr;
delete _reference_count;
}
}
}
};
int main()
{
SmartPointer<char> cp1(new char('a'));
SmartPointer<char> cp2(cp1);
SmartPointer<char> cp3;
cp3 = cp2;
cp3 = cp1;
cp3 = cp3;
SmartPointer<char> cp4(new char('b'));
cp3 = cp4;
}

数组和字符串区别

1
2
3
4
const char * arr = "123"; // 存放在常量区,不能修改值
char * brr = "123"; //和上面那个一样 也是存放在常量存储区 因为字符串默认就存储在常量存储区 不可修改
const char crr[] = "123"; //数组 应该存在 栈区域 但是加了 const 编译器会将他优化,存到常量区 不可修改
char drr[] = "123"; // 存放在栈区域

C++里是怎么定义常量的?常量存放在内存的哪个位置?

局部常量 存储在栈区,全局常量 存在全局存储区,字面值常量如字符串 存在常量区

const 修饰成员函数的目的

在函数尾部加上 const

  1. const 修饰的成员函数表明函数调用不会对对象数据成员做出任何更改
  2. 也不能调用该类中的没有用const修饰的成员函数
  3. const对象是不可以调用类中的非const成员函数

因此,如果确认不会对对象做更改,就应该为函数加上const限定。必须在成员函数的声明和定义处同时加上 const 关键字。

c++隐式类型转换

  • 对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换
  • 对于只含一个参数的构造函数的对象来说,函数调用可以直接传入该类型的参数,编译器会使用该参数调用构造函数生成临时对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "stdafx.h"
#include <string>
#include <iostream>
using namespace std ;
class BOOK //定义了一个书类
{
private:
string _bookISBN ; //书的ISBN号
float _price ; //书的价格

public:
//定义了一个成员函数,这个函数即是那个“期待一个实参为类类型的函数”
//这个函数用于比较两本书的ISBN号是否相同
bool isSameISBN(const BOOK & other ){
return other._bookISBN==_bookISBN;
}

//类的构造函数,即那个“能够用一个参数进行调用的构造函数”(虽然它有两个形参,但其中一个有默认实参,只用一个参数也能进行调用)
BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}
};

int main()
{
BOOK A("A-A-A");
BOOK B("B-B-B");

cout<<A.isSameISBN(B)<<endl; //正经地进行比较,无需发生转换

cout<<A.isSameISBN(string("A-A-A"))<<endl; //此处即发生一个隐式转换:string类型-->BOOK类型,借助BOOK的构造函数进行转换,以满足isSameISBN函数的参数期待。
cout<<A.isSameISBN(BOOK("A-A-A"))<<endl; //显式创建临时对象,也即是编译器干的事情。
}

C++函数栈空间的最大值

1M 可调整

extern“C”

C++ 调用 C 函数 需要 extern c 因为c语言 没有函数重载

new/delete与malloc/free的区别是什么

  1. 前者是 C++ 的关键字,后者是C语言的库函数
  2. 前者不需要设置分配的空间大小,会根据对象的大小自动分配内存,并且会自动调用对象的构造函数
  3. 前者直接返回对象的指针,而后者返回void指针
  4. 前者分配失败会抛异常,后者返回空指针
  5. 后者分配的堆上的内存,前者默认是调用后者分配,但是在c++中称为自由存储区,即可以自己重定义分配内存的方式

在默认情况下,new操作的自由存储区和堆等价,但是可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。

前者 c++ 的 关键字 后者 c 的内存分配的库函数,后者使用必须指明申请内存空间的大小。对于类类型的对象,后者不会调用构造函数和析构函数。

new申请的内存可以由free释放吗?

简单数据类型可以,int、float这种数组没问题,但对类中动态分配内存且要在析构函数中释放的则不行、会造成内存泄露,编译不会报错—运行有可能会内存泄露

因为new本质上先调用operator new() 先分配内存(malloc根据不同大小在不同区域(小于的,大于128字节文件映射区)申请内存),之后再调用构造函数,delete先调用析构函数,然后再调用free释放内存。Free传入参数是void指针,所以编译不会报错

用new申请一个数组delete怎么知道释放多少

new 申请数组时 会多申请四个字节的空间用于保存大小,这四个字节位置在申请的空间的起始四个字节。

怎样使类的实例只能使用new来生成

在栈上分配内存是 静态建立,我们定义一个变量的时候,默认在栈上分配,而new默认在堆上分配。因为是在栈上分配的内存,变量出栈会自动调用析构函数,所以编译器再看到定义的变量的时候会首先检查它的析构函数有没有,没有的话就会报错。因此可以将析构函数设置为 private 性质的 或者 protect 性质的,这样就不能静态定义变量。然后自己重新写一个 成员函数用来析构对象。

(重载 new 关键字并将它设置为 delete 就可以使类只能在栈上分配

在栈上分配就是定义函数局部变量嘛 这时候编译器会自动检测该变量是否有构造函数和析构函数, 如果没有 你就不能定义;但是new 分配内存时它就只管你是否有构造函数 所以 将析构函数设置为 private 即可 使得只能 new 构造 但是需要自己重新实现一个 析构函数

如果不想让他 new 构造 那就 重载 new 函数并把它设置为 delete

RTTI

实现原理就是在 虚表的 -1 项存储类型信息,在编译器打开RTTI选项后 就可以使用 typeid 方法在运行过程中获取类别信息 dynamic_cast就是通过这个原理实现的

c语言怎么进行函数调用的

每个函数在栈上都有一个栈帧。当调用函数时,首先要将调用方的 栈底指针ebp,返回地址即下一条将要执行的指针,还有其他cpu的关键寄存器值存入当前函数栈的栈帧,此步骤即保存现场,接着为调用函数分配新的栈帧,即将当前esp指针作为新函数栈的ebp,PC指针设置为调用函数的地址从而实现程序跳转,如果有参数,将函数入口参数从右往左压入新的栈帧。函数调用结束后释放当前函数栈帧,并按序恢复调用前的现场。

ESP指针 栈指针 EBP 栈底指针 PC 程序计数器 EXA乘加运算的临时值

会分配函数栈,在栈内进行函数执行,先将返回地址地址压栈,再把当前函数的esp指针压栈

C语言参数压栈顺序

从右往左

c++如何处理返回值

如果反之值可以使用 eax 等寄存器存储,直接用cpu寄存器传递。 如果需要的空间较大 创建临时变量,将临时变量的引用作为函数参数输入函数

c++拷贝构造函数能否值传递

不能,会无限循环

fork 函数

pid_t fork(void); 成功调用 fork函数后,fork会创建出一个新进程,他几乎和调用fork的进程一模一样,不同的是,父进程中,fork返回的是 子进程的pid , 子进程中fork返回的是 0 ;可以根据这两个来区分是子进程还是父进程 这样来并行的执行两个任务。