0%

c++学习笔记

c++学习笔记

A.语法基础

1.宏定义:

宏替换发生的4种情况:

1.文件包含,将源程序中的#include扩展到文件中正文,将包含.h的文件展开到#include所在处

2.条件编译:预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。

3.宏展开:预处理器将源程序文件中出现的对宏的引用展开(替换)成相应的宏定义,即本文所说的#define的功能,由预处理器来完成。
经过预处理器处理的源程序与之前的源程序有所有不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能。

2.C++ 接口(抽象类)

如果一个类中至少包含一个纯虚函数,则这个类是一个抽象类。

纯虚函数在声明时使用“=0”来指定。

3.c++的默认初始化、列表初始化、值初始化:

初始化不是赋值,初始化的含义是创建变量时赋予一个初始值;赋值的含义是把对象的当前值擦除,而以一个新值代替。

  • 默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,变量被赋予默认值,值由自己的类型确定。

如果内置数据类型未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0,定义在函数体内部的内置类型变量不被初始化,则其值为未定义(未初始化的变量含有一个不确定的值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

//函数体外定义的变量会默认初始化
int a;
int aa[3];

int main() {
//函数体内定义表的变量不会默认初始化
int b;
int bb[3];
for(auto i: aa ){
cout<<i<<endl;
}
for(auto i: bb ){
cout<<i<<endl;
}
cout<<a<<endl;
cout<<b<<endl;
return 0;
}

image-20220514170125341

绝大多数类支持无需显式初始化而定义对象,这样的类会为对象提供一个合适的默认值

1
std::string res;

res非显式的初始化为一个空字符串。

一些类要求对象显式初始化,如果创建该类的一个对象却未显式初始化,会引发错误。

默认初始化创建一个指定类型的空vector

1
vector<int> vec;

在后续使用可以高效的添加元素

  • 列表初始化

c++11提出的新标准,允许使用 用花括号括起来的 0个或多个初始元素 被赋予vector

1
vector<string> s = {"a","b","aba"};

4.链接

静态链接、动态链接

  • 静态链接

链接时将需要用到的函数或过程链接到生成的可执行文件中,就算把静态库删除也不会影响到可执行程序的执行,生成的静态链接库,windows下以.lib为后缀,linux下以.a为后缀。

  • 动态链接

链接的时候没有把需要用到的函数代码链接进去,而是在执行的过程中找要链接的函数,生成的可执行文件没有函数代码,只包含重定位信息,当你删除动态链接库后,可执行文件不能执行。动态链接库windows下为.dll,linux下为.so。

5.include <>和” “的区别

  1. <>的头文件是系统文件,””的头文件是自定义文件
  2. 编译器预处理阶段的查找路径不一样

查找路径

  • <> 编译器设置的头文件路径->系统变量
  • “”当前头文件目录->编译器设置的头文件路径->系统变量

6.说说const和define的区别

const用于定义常量

define用于定义宏,宏也可以用于定义常量

  1. const生效于编译的阶段,define生效于预处理阶段
  2. const定义的常量,是c语言存储在内存中,需要额外内存空间的,define定义的常量在运行时直接是操作数,不会放在内存中。
  3. const定义的常量是带类型的,define定义的常量是不带类型的,define定义的常量不利于类型检查。

7.const

1
2
3
4
5
6
const int a;//a是常量
const int *a;//a是指向int常量的指针
int const *a;//a是指向int常量的指针

int * const a;//a是指向int的常量指针
const int * const a;//a是指向常量int的常量指针
1
2
3
const * 是常量指针

* const 是指针常量

8.lamda表达式

  1. C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,用来简化编程工作。

  2. Lambda 的语法形式:

  3. [捕获列表] (参数列表) -> 返回值类型 { 函数体 };

o [捕获列表]:这部分是捕获区,用于捕获外部变量,标识一个 Lambda 表达式的开始,不能省略

捕获列表为空:不捕获外部变量

=:表示值传入

&:表示引用传入

示例:

[a]:将 a 按值进行传递

[&a]:将 a 按引用进行传递。

[a,&b]:将 a 按值传递,b 按引用进行传递。

[=,&a,&b]:除 a 和 b 按引用进行传递外,其它参数都按值进行传递。

[&,a,b]:除 a 和 b 按值进行传递外,其它参数都按引用进行传递。

o (参数列表):同函数参数列表

o -> 返回值类型:这部分如果返回值为 void,可以省略

o { 函数体 }:同函数的函数体

9.内联函数

内联函数(inline),通常就是在每个调用点上“内联地”展开,内联函数的使用可以有效避免函数调用的开销。

1
2
3
inline const string & shorterString(const string & s1,const string & s2){
return s1.size()<s2.size()?s1:s2;
}

内联机制用于优化规模较小、流程直接、频繁调用的函数。

大多数编译器不支持内联递归函数

10.constexpr函数

constexpr函数是指能用于常量表达式的函数,需要遵循以下几点原则;

1
2
constexpr int new_sz()  {return 42;}
constexpr int foo = new_sz;
  1. 函数返回值与形参类型必须是字面值类型
  2. 函数体中有且只有一条return语句
  3. 函数中也可由其他语句(空语句、类型别名、using声明),只要这些语句在运行时不执行其他操作。

执行constexpr函数时,编译器会把函数调用替换为其结果值;为了能在编译过程随时展开,constexpr函数会被隐式的指定为内联函数。

允许constexpr函数的返回值并非一个常量:

1
2
3
4
5
6
//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz*cnt;}

int arr[scale(2)]; //正确,scale(2)是常量表达式
int i = 2;
int a2[scale(2)]; //此处编译器报错,因为scale(2)不是常量表达式;而此处上下文需要scale返回一个常量表达式

如果cnt是常量表达式,则返回值也是常量表达式;反之不然。

tips:通常把内联函数和constexpr函数放在头文件内(因为对于某个给定的内联函数或constexpr函数来说,可以在程序中多次定义,但它的多个定义必须完全一致)

B.内存

1.C++内存

img

一个程序有哪些section:

如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆、共享区、栈等组成。

  1. 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
  2. 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
  3. BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。

存放未初始化或者初始化为0的全局变量和静态变量的一块内存区域,特点是可读写的。在程序执行前BSS段会自动清0。

  1. 可执行程序在运行时又会多出两个区域:堆区和栈区。

    堆区:动态申请内存用。堆从低地址向高地址增长。

    栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。

  2. 最后还有一个共享区,位于堆和栈之间。

程序启动的过程:

  1. 操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
  2. 加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
  3. 加载器针对该程序的每一个动态链接库调用LoadLibrary (1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。 (2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。 (3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3 (4)调用该动态链接库的初始化函数
  4. 初始化应用程序的全局变量,对于全局对象自动调用构造函数。
  5. 进入应用程序入口点函数开始执行。

怎么判断数据分配在栈上还是堆上:首先局部变量分配在栈上;而通过malloc和new申请的空间是在堆上。

C.面向对象

1.多态

使用基类的指针指向子类的对象,使用父类的指针调用子类的成员函数,实现多态。

包含重载、重写。

  1. 多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)
  2. 多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现。

2.虚函数

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

  1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
  2. 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
  3. 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
  4. 重写用虚函数来实现,结合动态绑定。
  5. 纯虚函数是虚函数再加上 = 0。
  6. 抽象类是指包括至少一个纯虚函数的类。

纯虚函数:virtual void fun()=0。即抽象类必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。

3.虚函数与纯虚函数的区别

  1. 只有虚函数的类不叫抽象类,但含有纯虚函数的类称为抽象类。

  2. 虚函数可以直接使用,被子类重写后可以以多态的形式调用;但纯虚函数不可以直接使用,因为其只有声明而没有定义。

  3. 虚函数和纯虚函数都可以在子类被重写,以多态的形式调用。

  4. 虚函数和纯虚函数通常存在于抽象基类中,被子类进行重写,目的是提供一个统一的接口。

  5. 虚函数定义 virtual{};纯虚函数定义virtual{}=0。

    虚函数和纯虚函数不能有static修饰符,因为被static修饰的函数在编译前要求前期绑定,但虚函数是动态绑定。

含有纯虚函数的类是抽象类,它不能生成对象,用户不能创建类的实例,只能创建其派生类的实例。

4.c++中的构造函数

默认构造函数

初始化构造函数

拷贝构造函数 复制构造函数默认的是浅拷贝

移动构造函数 将其他类型的变量隐式转为本类的对象

如果一个类定义了自己的析构函数,那么它要定义自己的拷贝构造函数和默认构造函数

5.简述一下c++中的静态多态与动态多态

  • 静态多态在编译期间完成,编译器根据实参类型选择相应的重载函数。如果找到重载函数则执行,找不到则编译时报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include<iostream>
using namespace std;

int Add(int a,int b)//1
{
return a+b;
}

char Add(char a,char b)//2
{
return a+b;
}

int main()
{
cout<<Add(666,888)<<endl;//1
cout<<Add('1','2');//2
return 0;
}
  • 动态多态

通过基类的指针指向子类对象实现多态。

动态绑定2条件:

  1. 虚函数,基类中必须有虚函数,在派生类中必须重写虚函数。
  2. 通过基类类型的指针引用来调用虚函数。

6.说说为什么要虚析构,为什么不能虚构造

  1. 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。

    1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
    2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。

    C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

    (简单来说,虚析构函数可以使基类指针析构子类的对象,实现多态的特性)

  2. 不能虚构造:

    1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)
    2. 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
    3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

7.定义析构函数会自动生成哪些函数

只定义析构函数,编译器会自动生成拷贝构造函数和默认构造函数。

拷贝构造函数默认使用浅拷贝。

8.一个类会默认生成哪些函数

定义一个空类,会默认生成以下函数:

1.无参构造函数

1
2
3
Empty()
{
}

2.拷贝构造函数

1
2
3
Empty(const Empty& copy)
{
}

3.赋值运算符

1
2
3
Empty& operator = (const Empty& copy)
{
}

4.析构函数(非虚)

1
2
3
~Empty()
{
}

9.说说c++类对象的初始化顺序

父类构造函数–>成员类对象构造函数–>自身构造函数

其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。

析构顺序和构造顺序相反。

10.简述浅拷贝和深拷贝

  1. 浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。
  2. 深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
  3. 深拷贝的实现:深拷贝的拷贝构造函数和赋值运算符的重载传统实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
STRING( const STRING& s )
{
//_str = s._str;
_str = new char[strlen(s._str) + 1];
strcpy_s( _str, strlen(s._str) + 1, s._str );
}
STRING& operator=(const STRING& s)
{
if (this != &s)
{
//this->_str = s._str;
delete[] _str;
this->_str = new char[strlen(s._str) + 1];
strcpy_s(this->_str, strlen(s._str) + 1, s._str);
}
return *this;
}

这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象 , 那么这里的赋值运算符的重载是怎么样做的呢?

image-20220515104206498

这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题。

11.请你回答一下 C++ 类内可以定义引用数据成员吗?

c++类内可以定义引用成员变量,但要遵循以下三个规则:

  1. 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误。
  2. 构造函数的形参也必须是引用类型。
  3. 不能在构造函数里初始化,必须在初始化列表中进行初始化。

12.简述一下什么是常函数,有什么作用

类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。

在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。

除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。

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
#include<iostream>
using namespace std;

class CStu
{
public:
int a;
CStu()
{
a = 12;
}

void Show() const
{
//a = 13; //常函数不能修改数据成员
cout <<a << "I am show()" << endl;
}
};

int main()
{
CStu st;
st.Show();
system("pause");
return 0;
}

13.说说什么是虚继承,解决什么问题,如何实现?

虚继承是解决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
32
33
#include<iostream>
using namespace std;
class A{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
//菱形继承和菱形虚继承的对象模型
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
cout << sizeof(D) << endl;
return 0;
}

分别从菱形继承和虚继承来分析:

img

菱形继承中A在B,C,D,中各有一份,虚继承中,A共享(虚继承使得菱形继承共享一个基类对象)。

上面的虚继承表实际上是一个指针数组。B、C实际上是虚基表指针,指向虚基表。

虚基表:存放相对偏移量,用来找虚基类

未命名绘图

14.说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?

参考回答

  1. 纯虚函数不可以实例化,但是可以用其派生类实例化,示例如下:

    1
    2
    3
    class Base { 
    public: virtual void func() = 0;
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include<iostream>  
    using namespace std;
    class Base {
    public:
    virtual void func() = 0;
    };
    class Derived :public Base {
    public:
    void func() override {
    cout << "哈哈" << endl;
    }
    };
    int main() {
    Base *b = new Derived();
    b->func();
    return 0;
    }
  2. 虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。

    即“纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”

    所以纯虚函数不能实例化。

  3. 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。

  4. 定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

15.请问拷贝构造函数的参数是什么传递方式,为什么

参考回答

  1. 拷贝构造函数的参数必须使用引用传递

  2. 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

    需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

16.如何理解抽象类?

参考回答

  1. 抽象类的定义如下:

    纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有纯虚函数的类就叫做抽象类。

  2. 抽象类有如下几个特点:

    1)抽象类只能用作其他类的基类,不能建立抽象类对象。

    2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。

    3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

17.说说什么是虚基类,可否被实例化?

参考回答

  1. 在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:

    1
    2
    3
    4
    class A
    class B1:public virtual A;
    class B2:public virtual A;
    class D:public B1,public B2;
  2. 虚继承的类可以被实例化,举例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Animal {
    int weight;
    int getWeight(){
    return weight;
    };

    };
    class Tiger : virtual public Animal { /* ... */ };
    class Lion : virtual public Animal { /* ... */ }
    1
    2
    3
    4
    5
    6
    7
    int main( )
    {
    Liger lg;

    /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,lg对象中只会有一个animal对象。于是下面的代码编译OK */
    int weight = lg.getWeight();
    }

18.简述一下拷贝赋值和移动赋值?

参考回答

  1. 拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。

  2. 移动赋值是通过移动构造函数来赋值,二者的主要区别在于

    1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;

    2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。

19.C++ 中哪些函数不能被声明为虚函数?

参考回答

常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

  1. 为什么C++不支持普通函数为虚函数?

    普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。

  2. 为什么C++不支持构造函数为虚函数?

    这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

    构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数

  3. 为什么C++不支持内联成员函数为虚函数?

    其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数

    内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数

  4. 为什么C++不支持静态成员函数为虚函数?

    这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。

    静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

  5. 为什么C++不支持友元函数为虚函数?

    因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

20.解释下 C++ 中类模板和模板类的区别

参考回答

  1. 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
  2. 模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。

答案解析

  1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template class someclass{…};在定义对象时分别代入实际的类型名,如 someclass obj;
  2. 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
  3. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

21. 虚函数表里存放的内容是什么时候写进去的?

参考回答

  1. 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入
  2. 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。

D. c++11新特性

1.说说 C++11 的新特性有哪些

C++新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下11点:

  1. 语法的改进

    (1)统一的初始化方法

    (2)成员变量默认初始化

    (3)auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)

    (4)decltype 求表达式的类型

    (5)智能指针 shared_ptr

    (6)空指针 nullptr(原来NULL)

    (7)基于范围的for循环

    (8)右值引用和move语义 让程序员有意识减少进行深拷贝操作

  2. 标准库扩充(往STL里新加进一些模板类,比较好用)

    (9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高

    (10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串

    (11)Lambda表达式

2.智能指针

C++ 提供了四个智能指针模板类, 分别是: auto_ptr, unique_ptr, shared_ptrweak_ptr. (auto_ptr是 C++98 提供的解决方案, C++11 已经将其摒弃, 并提供了另外三种解决方案). 这三个智能指针模板都定义了类似指针的对象, 可以将new获得(直接或间接)的地址赋给这种对象. 当智能指针过期时, 其析构函数将使用delete来释放内存. (要创建智能指针对象, 需要包含头文件<memory>)

1.三种智能指针的区别?

  • auto_ptr: 当进行赋值时, 会将旧指针的所有权转让, 使得 对于特定的对象, 只能有一个智能指针可以拥有它.(c++11已摒弃)
  • unique_ptr: 当进行赋值时, 会将旧指针的所有权转让, 使得 对于特定的对象, 只能有一个智能指针可以拥有它. unique_ptr 相比于 auto_ptr 会执行更加严格的所有权转让策略
  • shared_ptr: 通过引用计数(reference counting), 跟踪引用特定特定对象的智能指针数. 当发生赋值操作时, 计数增1, 当指针过期时, 计数减1. 仅当最后一个指针过期时, 才调用 delete.
  • weak_ptr: 不控制对象声明周期的智能指针, 它指向一个 shared_ptr 管理的对象, 而进行内存管理的只有 shared_ptr. weak_ptr 主要用来帮助解决循环引用问题, 它的构造和析构不会引起引用计数的增加或者减少. weak_ptr 一般都是配合 shared_ptr 使用, 通常不会单独使用.

2.unique_ptr 和 auto_ptr 的区别?

  • 所有权转让机制不同: auto_ptr 允许通过直接赋值进行转让, 但是这样会留下危险的 悬挂指针, 容易使得程序在运行阶段崩溃. unique_ptr 仅仅允许将临时右值进行赋值, 否则会在编译阶段发生错误, 这样更加安全(编译阶段错误比潜在的程序崩溃更安全).

  • 相比于 auto_ptr 和 share_ptr, unique_ptr 可以使用new[]分配的内存作为参数:

    1
    std::unique_ptr<double[]> pda(new double(5));

3.如何选择合适的智能指针?

如果程序要使用多个指向同一个对象的指针, 应选择shared_ptr, 这样的情况包括:

  • 对于智能指针数组, 用辅助指针来标识最大值或最小值的情况
  • 很多 STL 算法都支持复制和赋值操作, 这些操作可用于 shared_ptr, 但不能用于 unique_ptrauto_ptr.

如果程序不需要多个指向同一个对象的指针, 则可以使用unique_ptr, 如果函数使用new分配内存, 并返回指向该内存的指针, 将其返回类型声明为unique_ptr是不错的选择, 这样, 所有权将转让给接受返回值的unique_ptr.

3.无序容器(哈希表)

无序容器(哈希表)

用法和功能同map一模一样,区别在于哈希表的效率更高。

(1) 无序容器具有以下 2 个特点:

a. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,

b. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。

(2) 和关联式容器一样,无序容器只是一类容器的统称,其包含有 4 个具体容器,分别为 unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。功能如下表:

截屏2022-05-22 16.29.46

3.正则表达式

可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串。常用符号的意义如下:

符号 意义
^ 匹配行的开头
$ 匹配行的结尾
. 匹配任意单个字符
[…] 匹配[]中的任意一个字符
(…) 设定分组
\ 转义字符
\d 匹配数字[0-9]
\D \d 取反
\w 匹配字母[a-z],数字,下划线
\W \w 取反
\s 匹配空格
\S \s 取反
+ 前面的元素重复1次或多次
* 前面的元素重复任意次
? 前面的元素重复0次或1次
{n} 前面的元素重复n次
{n,} 前面的元素重复至少n次
{n,m} 前面的元素重复至少n次,至多m次
\ 逻辑或

5.Lambda匿名函数

所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。

(1)定义

lambda 匿名函数很简单,可以套用如下的语法格式:

​ [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; };

其中各部分的含义分别为:

a. [外部变量方位方式说明符] [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。

b. (参数) 和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;

c. mutable 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

注意:对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;

d. noexcept/throw() 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

e. -> 返回值类型 指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略”-> 返回值类型”。

f. 函数体 和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。

(2)程序实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num){
cout << n << " ";
}
return 0;
}

/* 程序运行结果:
1 2 3 4
*/