C++ 基础知识[二]

目录
警告
本文最后更新于 2023-07-15,文中内容可能已过时。
quote
c++ 八股文 第一部分

6 基础知识(六)

6.1 构造函数为什么不能定义为虚函数? ⽽析构函数⼀般写成虚函数的原因 ?

构造函数不能声明为虚函数的原因是:

1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?) 编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。

析构函数设为虚函数的作用: 解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。(如果基类的析构函数不是虚函数,那么在delete 基类指针时,只调用基类的析构函数,不会调用派生类的析构函数,故派生类部分不会被析构。)

6.2 c/c++中register关键字(寄存器、缓存、内存)

c/c++中register关键字(寄存器、缓存、内存)

一般情况下,变量的值是存储在内存中的,CPU 每次使用数据都要从内存中读取。如果有一些变量使用非常频繁,从内存中读取就会消耗很多时间,例如 for 循环中的增量控制。

为了解决这个问题,可以将使用频繁的变量放在CPU的通用寄存器中,这样使用该变量时就不必访问内存,直接从寄存器中读取,大大提高程序的运行效率。

寄存器、缓存、内存

为了加深对 register 变量的理解,这里有必要讲一下CPU寄存器。

按照与CPU的远近来分,离CPU最近的是寄存器,然后是缓存,最后是内存。

寄存器是最贴近CPU的,而且CPU只在寄存器中进行存取。寄存的意思是暂时存放数据,不用每次都从内存中读取,它是一个临时的存放数据的空间

而寄存器的数据又来源于内存,于是 CPU <– 寄存器 <– 内存,这就是它们之间的信息交换。

那么为什么还需要缓存呢?因为如果频繁地操作内存中同一地址上的数据会影响速度,于是就在寄存器和内存之间设置一个缓存,把使用频繁的数据暂时保存到缓存,如果寄存器需要读取内存中同一地址上的数据,就不用大老远地再去访问内存,直接从缓存中读取即可。

缓存的速度远高于内存,价格也是如此。

注意:缓存的容量是有限的,寄存器只能从缓存中读取到部分数据,对于使用不是很频繁的数据,会绕过缓存,直接到内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。

关于缓存的命中率又是一门学问,哪些数据保留在缓存,哪些数据不保留,都有复杂的算法。

注意:上面所说的CPU是指CPU核心,从市场上购买的CPU已是封装好的套件,附带了寄存器和缓存,插到主板上就可以用。

从经济和速度的综合考虑,缓存又被分为一级缓存、二级缓存和三级缓存,它们的存取速度和价格依次降低,容量依次增加。购买到的CPU一般会标出三级缓存的容量。

register 变量

寄存器的数量是有限的,通常是把使用最频繁的变量定义为 register 的。

关于寄存器变量有以下事项需要注意:

  • 为寄存器变量分配寄存器是动态完成的,因此,只有局部变量和形式参数才能定义为寄存器变量。
  • 局部静态变量不能定义为寄存器变量,因为一个变量只能声明为一种存储类别。
  • 寄存器的长度一般和机器的字长一致,所以,只有较短的类型如int、char、short等才适合定义为寄存器变量,诸如double等较大的类型,不推荐将其定义为寄存器类型。
  • CPU的寄存器数目有限,因此,即使定义了寄存器变量,编译器可能并不真正为其分配寄存器,而是将其当做普通的auto变量来对待,为其分配栈内存。当然,有些优秀的编译器,能自动识别使用频繁的变量,如循环控制变量等,在有可用的寄存器时,即使没有使用 register 关键字,也自动为其分配寄存器,无须由程序员来指定。

c++中register

在早期c语言编译器不会对代码进行优化,因此使用register关键字修饰变量是很好的补充,大大提高的速度。

register关键字请求让编译器将变量a直接放入寄存器里面,以提高读取速度,在C语言中register关键字修饰的变量不可以被取地址,但是c++中进行了优化。

c++中依然支持register关键字,但是c++编译器也有自己的优化方式,即某些变量不用register关键字进行修饰,编译器也会将多次连续使用的变量优化放入寄存器中,例如入for循环的循环变量i。

c++中也可以对register修饰的变量取地址,不过c++编译器发现程序中需要取register关键字修饰的变量的地址时,register关键字的声明将变得无效。

6.3 c/c++中进程和线程的区别

c++多线程编程 – 进程与线程区别 c++面试-操作系统篇 面试必考 | 进程和线程的区别

  • 何为进程(process)?

    • 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个过程。简单来说,进程是程序(应用程序,可执行文件)的一次执行。进程通常由程序、数据和进程控制块(PCB)组成。比如双击打开一个桌面应用软件就是开启了一个进程。

    • 传统的进程有两个基本属性:可拥有资源的独立单位;可独立调度和分配的基本单位。对于这句话我的理解是:进程可以获取操作系统分配的资源,如内存等;进程可以参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。

    • 进程在创建、撤销和切换中,系统必须为之付出较大的时空开销,因此在系统中开启的进程数不宜过多。比如你同时打开十几个应用软件试试,电脑肯定会卡死的。于是紧接着就引入了线程的概念。

  • 何为线程(thread)?

    • 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也有说,线程是CPU可执行调度的最小单位。也就是说,进程本身并不能获取CPU时间,只有它的线程才可以。

    • 引入线程之后,将传统进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。我对这句话的理解是:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。而进程负责获取操作系统分配的资源,如内存。

    • 线程基本上不拥有资源,只拥有一点运行中必不可少的资源,它可与同属一个进程的其他线程共享进程所拥有的全部资源。

    • 线程具有许多传统进程所具有的特性,故称为“轻量型进程”。同一个进程中的多个线程可以并发执行。

  • 进程和线程的区别?

    • 线程分为用户级线程内核支持线程两类,用户级线程不依赖于内核,该类线程的创建、撤销和切换都不利用系统调用来实现; 内核支持线程依赖于内核,即无论是在用户进程中的线程,还是在系统中的线程,它们的创建、撤销和切换都利用系统调用来实现。

    • 但是,与线程不同的是,无论是系统进程还是用户进程,在进行切换时,都要依赖于内核中的进程(process)调度。因此,无论是什么进程都是与内核有关的,是在内核支持下进程切换的。尽管线程和进程表面上看起来相似,但是他们在本质上是不同的。

    • 根据操作系统中的知识,进程至少必须有一个线程,通常将此线程称为主线程。

    • 进程要独立地占用系统资源(如内存),而同一进程的线程之间是共享资源的。进程本身并不能获取CPU时间,只有它的线程才可以。

  • 其他

    • 进程在创建、撤销和切换过程中,系统的时空开销非常大。用户可以通过创建线程来完成任务,以减少程序并发执行时付出的时空开销。例如可以在一个进程中设置多个线程,当一个线程受阻时,第二个线程可以继续运行,当第二个线程受阻时,第三个线程可以继续运行……。这样,对于拥有资源的基本单位(进程),不用频繁的切换,进一步提高了系统中各种程序的并发程度。

ref:
[1].https://blog.csdn.net/qq_41803340/category_10405604.html

[待整理内容]

一.常考C++基础概念

1.C++三大特性(封装、继承、多态)

封装:

隐藏类的属性和实现细节,仅仅对外提供接口, 封装性实际上是由编译器去识别关键字public、private和protected来实现的, 体现在类的成员可以有公有成员(public)私有成员(private)保护成员(protected)。 私有成员是在封装体内被隐藏的部分,只有类体内声明的函数(类的成员函数)才可以访问私有成员, 而在类体外的函数是不能访问的,公有成员(public)是封装体与外界的一个接口, 类体外的函数可以访问公有成员,保护成员是只有该类的成员函数和该类的派生类才可以访问的。

优点:隔离变化;便于使用;提高重用性;提高安全性 缺点:如果封装太多,影响效率;使用者不能知道代码具体实现。

继承:

被继承的是父类(基类),继承出来的是子类(派生类),子类拥有父类的所有的特性。 继承方式有公有继承私有继承保护继承。默认是私有继承

*公有继承中父类的公有和保护成员在子类中不变,私有的在子类中不可访问。 *私有继承中父类的公有和保护成员在子类中变为私有,但私有的在子类中不可访问。 *保护继承中父类的公有和保护成员在子类中变为保护,但私有的在子类中不可访问。 c++语言允许单继承和多继承

优点:继承减少了重复的代码、继承是多态的前提、继承增加了类的耦合性; 缺点:继承在编译时刻就定义了,无法在运行时刻改变父类继承的实现;

父类通常至少定义了子类的部分行为,父类的改变都可能影响子类的行为; 如果继承下来的子类不适合解决新问题,父类必须重写或替换,那么这种依赖关系就限制了灵活性, 最终限制了复用性。

虚继承:为了解决多重继承中的二义性问题,它维护了一张虚基类表。 (菱形继承问题)

多态:

ref: 多态的四种表现形式

  • 运行时多态(虚函数)
  • 编译时多态(模板)
  • 重载
  • 类型转换

运行时多态(Subtype Polymorphism/Runtime Polymorphism)

运行时多态就是派生类重写基类的虚函数,在调用函数里,参数为基类的指针或引用,会构成多态。我之前写过一篇多态的原理,就是在讲多态(运行时多态)在底层是怎么实现的 多态的底层实现

举个例子:比如买票这个行为,成人去买就是全价,学生买就是半价票。但是不管成人还是学生都是人这个体系。所以我们需要根据谁来买票才能决定价格,这个时候就需要多态。

 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
#include <iostream>

class ticket
{
public:
	virtual void price() = 0;
};

class adult : public ticket
{
public:
	virtual void price() override
	{
		std::cout << "成人全价!" << std::endl;
	}
};

class student : public ticket
{
public:
	virtual void price() override
	{
		std::cout << "学生半价!" << std::endl;
	}
};

void BuyTicket(ticket& t)
{
	t.price();
}

int main(void)
{
	adult a;
	student s;

	BuyTicket(a);
	BuyTicket(s);
	return 0;
}

编译时多态(Parametric Polymorphism/Compile-Time Polymorphism)

编译时多态就是模板。在程序编译时,编译器根据参数的类型,就将生成某种类型的函数或类。我之前关于模板的(总结)[https://blog.csdn.net/weixin_42678507/article/details/88658291]

举个简单的例子:Add() 函数是一个非常简单的函数,但是如果你写一个整型的 Add 函数,那么我想加 double 型的呢?你再写一个 double 型的 Add 函数,那么我想加 char 型的呢?

这个时候就用到了模板,我们先定义一个逻辑,具体类型等编译时再生成该类型的函数或类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

template<class T>
T Add(T lhs, T rhs)
{
	return lhs + rhs;
}

int main(void)
{
	Add(1, 2);
	Add(2.0, 3.0);
	Add('a', 'b');
	return 0;
}

重载(Ad-hoc Polymorphism/Overloading)

函数名相同,参数不同就构成了重载。重载主要用于函数,当某个函数的功能无法处理某些参数的情况时,我们就可以重载一个函数来单独处理。

举个例子:比如说上面的 Add 函数,当前内置类型都可以处理,但是如果我传两个字符串怎么办?就不可以像刚才那么加了。得重载一个函数单独处理。

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

int Add(int lhs, int rhs)
{
	return lhs + rhs;
}

std::string Add(const std::string& lhs, const std::string& rhs)
{
	std::string ans(lhs);
	ans += rhs;

	return ans;
}

int main(void)
{
	Add(1, 2);
	Add("abc", "def");

	return 0;
}

类型转换(Coercion Polymorphism/Casting)

类型转换主要分为四种:

  • static_cast: 相当于隐式类型转换。
  • const_cast: 这个可以去除一个 const 变量的 const 性质,使可以改变它的值。
  • reinterpret_cast: 相当于强制类型转换。
  • dynamic_cast: 这个可以使子类指针或引用赋值给父类指针或引用。

类型转换很简单,这里就不多赘述了。

2.数组和链表的区别

数组和链表是两种不同的数据存储方式

数组的定义

数组是一组具有相同数据类型的变量的集合,这些变量称之为集合的元素。 每个元素都有一个编号,称之为下标,可以通过下标来区别并访问数组元素,数组元素的个数叫做数据的长度。

链表的定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表的特性是在中间任意位置插入和删除元素都非常快,不需要移动其它元素。 对于单向链表而言,链表中的每一个元素都要保存一个指向下一个元素的指针。 对于双向链表而言,链表中的每个元素既要保存指向下一个元素的指针,又要保存指向上一个元素的指针。 对于双向循环链表而言,链表中的最后一个元素保存一个指向第一个元素的指针。

数组和链表的区别主要表现在以下几个方面

比较数组链表
逻辑结构(1) 数组在内存中连续; (2)使用数组之前,必须事先固定数组长度,不支持动态改变数组大小; (3) 数组元素增加时,有可能会数组越界; (4) 数组元素减少时,会造成内存浪费; (5)数组增删时需要移动其它元素(1) 链表采用动态内存分配的方式,在内存中不连续 (2)支持动态增加或者删除元素 (3) 需要时可以使用malloc或者new来申请内存,不用时使用free或者delete来释放内存
内存结构数组从栈上分配内存,使用方便,但是自由度小链表从堆上分配内存,自由度大,但是要注意内存泄漏
访问效率数组在内存中顺序存储,可通过下标访问,访问效率高链表访问效率低,如果想要访问某个元素,需要从头遍历
越界问题数组的大小是固定的,所以存在访问越界的风险越界的风险 只要可以申请得到链表空间,链表就无越界风险

数组和链表的使用场景

比较数组使用场景链表使用场景
空间数组的存储空间是栈上分配的,存储密度大,当要求存储的大小变化不大时,且可以事先确定大小,宜采用数组存储数据链表的存储空间是堆上动态申请的,当要求存储的长度变化较大时,且事先无法估量数据规模,宜采用链表存储
时间数组访问效率高。当线性表的操作主要是进行查找,很少插入和删除时,宜采用数组结构链表插入、删除效率高,当线性表要求频繁插入和删除时,宜采用链表结构

3. 智能指针

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:newdelete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

  • 1 智能指针的作用

    • 智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象生命周期结束时,自动调用析构函数释放资源
  • 2 智能指针的种类: shared_ptr、unique_ptr、weak_ptr、auto_ptr

2.1 智能指针的实现原理

智能指针的实现原理就是在一个类的内部封装了类对象的指针,然后在析构函数里对我们的类对象指针进行释放,因为类的析构是在类对象生命期结束时自动调用的,这样我们就省去了手动释放内存的操作,避免忘记手动释放导致的内存泄漏。

2.2 C++11四种智能指针总结

2.2.1 auto_ptr:

auto_ptr以前是用在C98中,C++11被抛弃,头文件一般用来作为独占指针

auto_ptr被赋值或者拷贝后,失去对原指针的管理

auto_ptr不能管理数组指针,因为auto_ptr的内部实现中,析构函数中删除对象使用delete而不是delete[],释放内存的时候仅释放了数组的第一个元素的空间,会造成内存泄漏。

auto_ptr不能作为容器对象,因为STL容器中的元素经常要支持拷贝,赋值等操作。

2.2.2 unique_ptr:

C++11中用来替代auto_ptr

拷贝构造和赋值运算符被禁用,不能进行拷贝构造和赋值运算

虽然禁用了拷贝构造和赋值运算符,但unique_ptr可以作为返回值,用于从某个函数中返回动态申请内存的所有权,本质上是移动拷贝,就是使用std:move()函数,将所有权转移。

2.2.3 share_ptr:

多个指针可以指向相同的对象,调用release()计数-1,计数0时资源释放

.use_count()查计数

.reset()放弃内部所有权

share_ptr多次引用同一数据会导致内存多次释放

循环引用会导致死锁,

引用计数不是原子操作。

shared_ptr 有两个数据成员,一个是指向 对象的指针 ptr,另一个是 ref_count 指针(包含vptr、use_count、weak_count、ptr等); 在这里插入图片描述

1
2
shared_ptr<Foo> x(new Foo);
shared_ptr<Foo> y = x;

步骤一:

`y=x` 涉及两个成员的复制,这两步拷贝不会同时(原子)发生,中间步骤 1,复制 ptr 指针,中间步骤 2,复制 ref_count 指针,导致引用计数加 1

步骤二:

因为是两步,如果没有 mutex 保护,那么在多线程里就有数据竞争。

多线程读写同一个 shared_ptr 必须加锁。

2.2.4 weak_ptr:

1.解决两个share_ptr互相引用产生死锁,计数永远降不到0,没办法进行资源释放,造成内存泄漏的问题。

2.使用时配合share_ptr使用,把其中一个share_ptr更换为weak_ptr。

4. 重载、重写、重定义

(1) 重载(overload): 指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。 a 相同的范围(在同一个类中) b 函数名字相同、 参数不同 c virtual关键字可有可无 d 返回值可以不同;

(2) 重写(覆盖override)是指派生类函数覆盖基类函数,特征是: a 不同的范围,分别位于基类和派生类中 b 函数的名字相同、 参数相同 c 基类函数必须有virtual关键字,不能有static d 返回值相同(或者协变),否则报错; e 重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public, protected也是可以的

(3) 重定义(隐藏redefine)是指派生类的函数屏蔽了与其同名的基类函数,特征是: a 不在同一个作用域(分别位于派生类与基类) b 函数名字相同 c 返回值可以不同 d 规则:

如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏;

如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。

ps: 多态性可以分为静态多态性(方法的重载,一个类)和动态多态性(方法的覆盖,有继承关系的类之间的行为)。进而多态性可以由重载和覆盖来实现。

5.static与const区别和作用

static:

1.**static局部变量**将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完不会释放,而是继续保留在内存中;
2.**static全局变量**表示一个变量在当前文件的全局可以访问;
3.**static函数**表示一个函数只能在当前文件中被访问;
4.**static类成员变量**表示这个成员为全类所共有;
5.**static类成员函数**表示这个函数为全类所有,且只能访问成员变量。
6.全局变量在整个工程文件内有效;静态全局变量只在定义它的文件中有效;
7.静态局部变量只在定义它的函数内有效,且程序只分配一次内存,函数返回时不会释放,下次调用时不会重新赋值,还保留上次结果值;局部变量在函数返回时就释放掉;
8.全局变量和静态变量编译器会默认初始化为0;局部变量的默认值未知;
9.局部静态变量与全局变量共享全局数据,但是静态局部变量值在定义该变量的函数内部可见。
10.静态成员(静态成员函数)与非静态成员(成员函数)的区别在于有无this指针;静态成员是静态存储,必须进行初始化;
11.静态成员函数访问非静态成员报错: 静态成员在类加载时就已经分配内存,而此时非静态成员尚未分配内存,访问不存在的内存自然会报错;

const

1.<font color=red>const常量</font> 定义时必须初始化,以后不能修改;
2.<font color=red>const形参</font> 该形参在函数里不能被修改;
3.<font color=red>const修饰类成员函数</font> 该函数对成员变量只能进行读操作;

static关键字作用

1.函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此该值在下次调用时还维持上一次的值;
2.在模块内的static函数和变量可以被可以被模块内的函数访问,不能被模块外的函数访问;
3.在类内的static成员变量为整个类所有,类的所有对象只有一份拷贝;
4.在类内的static成员函数为整个类所有,这个函数不接收this指针,因此只能访问类的static成员变量;

const关键字

1.阻止一个变量被改变;
2.声明常量指针和指针常量;
3.const修饰形参,表示为输入参数,在函数体内不能修改该参数的值;
4.const修饰成员函数,表明为一个常函数,不能修改成员变量的值;
5.类的成员函数,有时必须返回const类型的值,使得返回值不能为左值。

const修饰指针有三种情况

  1. const修饰指针 — 常量指针 (const修饰的是指针,指针指向可以改,指针指向的值不可以更改)
1
2
3
    const int * p1 = &a;
    p1 = &b; //正确
    //*p1 = 100; 报错
  1. const修饰常量 — 指针常量 (const修饰的是常量,指针指向不可以改,指针指向的值可以更改)
1
2
3
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
  1. const即修饰指针,又修饰常量 (const既修饰指针又修饰常量,都不可以改)
1
2
3
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误

技巧:看const右侧紧跟着的是指针还是常量, 是指针就是常指针,是常量就是指针常量

6. const与宏定义(#define)区别和作用

const 定义的是变量不是常量,只是这个变量的值不允许改变,是常变量,带有类型。编译运行的时候起作用,存在类型检查。

define 定义的是不带类型的常数,只进行简单的字符替换。在预编译的时候起作用,不存在类型检查。

1、两者的区别 (1) 编译器处理方式不同 #define 宏是在预处理阶段展开。 const 常量是编译运行阶段使用。

(2) 类型和安全检查不同 #define 宏没有类型,不做任何类型检查,仅仅是展开。 const 常量有具体的类型,在编译阶段会执行类型检查。

(3) 存储方式不同 #define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。) const常量会在内存中分配(可以是堆中也可以是栈中)。

(4) const 可以节省空间,避免不必要的内存分配。 例如: const 定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象 #define 一样给出的是立即数,所以,const 定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而 #define 定义的常量在内存中有若干个拷贝。

(5) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

(6) 宏替换只作替换,不做计算,不做表达式求解;宏预编译时就替换了,程序运行时,并不分配内存。计算时注意边缘效应

7.虚函数和纯虚函数区别

1.虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
2.虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
3.虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
4.虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
5.虚函数的定义形式:virtual{};纯虚函数的定义形式:virtual  { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
6.虚函数充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说,它们都是虚函数。难怪有人甚至称虚函数是C++语言的精髓。
7.定义纯虚函数就是为了让基类不可实例化,因为实例化这样的抽象数据结构本身并没有意义或者给出实现也没有意义。

纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。

虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现

虚函数: https://www.cnblogs.com/zkfopen/p/11061414.html

8. 指针和引用的区别

1.指针和引用的定义和性质区别:

(1) 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

1
2
3
int a=1;int *p=&a;

int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

(3)可以有const指针,但是没有const引用;

(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(7)“sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(8)指针和引用的自增(++)运算意义不一样;

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

9. 结构体赋值

(结构体赋值)[https://blog.csdn.net/datase/article/details/78988320]

10. C和C++区别

(C和C++区别)[https://blog.csdn.net/czc1997/article/details/81254971]

11. C和C++传参方式区别

C语言不支持引用传参,如果想要改变传入参数的值,只能用传入指针的方式。

12. 深拷贝和浅拷贝区别

(深拷贝和浅拷贝区别)[https://blog.csdn.net/Situo/article/details/110225143]

13. 避免头文件重复包含以及宏定义重定义

1
2
3
#ifndef LWIP_TCP_KEEPALIVE
#define LWIP_TCP_KEEPALIVE
#endif

14. 你怎么理解虚拟类?虚拟类可以实例化一个对象吗?为什么?它的作用和其他类的区别

答案:虚拟类可以派生对象,纯虚类不可以实例化对象。因为纯虚类存在未定义的函数,只是个概念,不可真实存在。虚拟类用做多态,纯虚类做接口。

15. 内联函数怎么实现的,什么时期处理的,优缺点

答案:在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替换。 优点:不会产生函数调用的开销 缺点:增加目标程序的代码量,即增加空间开销

16 .位运算(按位与、按位或、异或)

按位与运算符(&)

参加运算的两个数,按二进制位进行“与”运算。

运算规则:只有两个数的二进制同时为1,结果才为1,否则为0。(负数按补码形式参加按位与运算)

即 0 & 0= 0 ,0 & 1= 0,1 & 0= 0, 1 & 1= 1。

例:3 &5 即 00000011 & 00000101 = 00000001 ,所以 3 & 5的值为1。

按位或运算符(|)

参加运算的两个数,按二进制位进行“或”运算。

运算规则:参加运算的两个数只要两个数中的一个为1,结果就为1。

即 0 | 0= 0 , 1 | 0= 1 , 0 | 1= 1 , 1 | 1= 1 。

例:2 | 4 即 00000010 | 00000100 = 00000110 ,所以2 | 4的值为 6 。 异或运算符(^)

参加运算的两个数,按二进制位进行“异或”运算。

运算规则:参加运算的两个数,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。

即 0 ^ 0=0 , 0 ^ 1= 1 , 1 ^ 0= 1 , 1 ^ 1= 0 。

例: 2 ^ 4 即 00000010 ^ 00000100 =00000110 ,所以 2 ^ 4 的值为6 。

17. 原码、反码、补码

原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。

反码:正数的反码还是等于原码 负数的反码就是他的原码除符号位外,按位取反。

补码:正数的补码等于他的原码 负数的补码等于反码+1。

18 . 堆和栈

(堆和栈)[https://blog.csdn.net/qq_45856289/article/details/106473750]

19. 类和对象

面向对象(Object Oriented,OO)。

起初,“面向对象”是指在程序设计中采用封装、继承、多态等设计方法。现在,面向对象的思想已经涉及到软件开发的各个方面。如,面向对象的分析(OOA,ObjectOriented Analysis),面向对象的设计(OOD,Object Oriented Design)、以及面向对象的编程实现(OOP,Object Oriented Programming)。 对象和类解释:

1)对象:对象是人们要进行研究的任何事物,它不仅能表示具体的事物,还能表示抽象的规则、计划或事件。对象具有状态,一个对象用数据值来描述它的状态。对象还有操作,用于改变对象的状态,对象及其操作就是对象的行为。对象实现了数据和操作的结合,使数据和操作封装于对象的统一体中。

2)类:具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象,类实际上就是一种数据类型。类具有属性,它是对象的状态的抽象,用数据结构来描述类的属性。类具有操作,它是对象的行为的抽象,用操作名和实现该操作的方法来描述。 对象和类的关系:

类与对象的关系就如模具和铸件的关系,类的实力化的结果就是对象,而对对象的抽象就是类,类描述了一组有相同特性(属性)和相同行为的对象。

20 . new和malloc区别

0.属性 new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

1.参数 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸

2.返回类型 new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。 而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

3.分配失败 new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

4.自定义类型 new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。 malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

5.重载 C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

6.内存区域 new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中

21. 内核链表与双向循环链表

(内核链表与双向循环链表)[https://blog.csdn.net/liebao_han/article/details/53956609]

22. 结构体和类的区别

1.结构体是一种值类型,而类是引用类型。值类型用于存储数据的值,引用类型用于存储对实际数据的引用。 那么结构体就是当成值来使用的,类则通过引用来对实际数据操作。

  1. 结构体使用栈存储(Stack Allocation),而类使用堆存储(Heap Allocation) 栈的空间相对较小.但是存储在栈中的数据访问效率相对较高. 堆的空间相对较大.但是存储在堆中的数据的访问效率相对较低.

3.类是反映现实事物的一种抽象,而结构体的作用只是一种包含了具体不同类别数据的一种包装,结构体不具备类的继承多态特性

4.结构体赋值是 直接赋值的值. 而对象的指针 赋值的是对象的地址

5.Struct变量使用完之后就自动解除内存分配,Class实例有垃圾回收机制来保证内存的回收处理。

6.结构体的构造函数中,必须为结构体所有字段赋值,类的构造函数无此限制

首先,关于隐式构造函数.我们知道,在1个类中如果我们没有为类写任意的构造函数,那么C++编译器在编译的时候会自动的为这个类生成1个无参数的构造函数.我们将这个构造函数称之为隐式构造函数 但是一旦我们为这个类写了任意的1个构造函数的时候,这个隐式的构造函数就不会自动生成了.在结构体中,就不是这样了,在结构体中隐式的构造函数无论如何都存在。所以程序员不能手动的为结构添加1个无参数的构造函数。

7.结构体中声明的字段无法赋予初值,类可以:

如何选择结构体还是类

1. 堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些 2. 结构表示如点、矩形和颜色这样的轻量对象,例如,如果声明一个含有 1000 个点对象的数组,则将为引用每个对象分配附加的内存。在此情况下,结构的成本较低。 3. 在表现抽象和多级别的对象层次时,类是最好的选择 4. 大多数情况下该类型只是一些数据时,结构时最佳的选择

23. 结构体和联合体区别

两者最大的区别在于内存利用

一、结构体struct

各成员各自拥有自己的内存,各自使用互不干涉,同时存在的,遵循内存对齐原则。一个struct变量的总长度等于所有成员的长度之和。

二、联合体union

各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权(对该内存的读写),各变量共用一个内存首地址。因而,联合体比结构体更节约内存。一个union变量的总长度至少能容纳最大的成员变量,而且要满足是所有成员变量类型大小的整数倍。

24. 结构体和枚举

一、结构体

结构体:很像面向对象中的对象,但是结构体没有方法只有属性,一个结构体由不同类型的元素组成,而相较于数组来说,数组只能存储相同类型的元素。结构体占用的空间等于内部各元素占用空间的和,并且元素在内存中的地址(按照元素定义的顺序)是连续的。

注意:结构体不能像面向对象中那样递归调用,自己包含自己,但是可以包含其他类型的结构体。

二、枚举

枚举:和面向对象中一样,枚举都是用来定义一些固定取值的常量,但是C中的枚举中的值是整数,默认按照0递增,也可以在定义枚举的时候赋值,那么后面的元素的值就会以这个元素为第一个元素递增

25 . 数组和指针的区别与联系

(数组和指针的区别与联系)[https://blog.csdn.net/cherrydreamsover/article/details/81741459]

26 . 函数指针&指针函数

https://blog.csdn.net/baidu_37973494/article/details/83150266

27 . const放在函数前后的区别

1、int GetY() const; 2、const int * GetPosition();

对于1 该函数为只读函数,不允许修改其中的数据成员的值。

对于2 修饰的是返回值,表示返回的是指针所指向值是常量

28 . goto语句

goto语句也称为无条件转移语句,其一般格式如下: goto 语句标号; 其中语句标号是按标识符规定书写的符号, 放在某一语句行的前面,标号后加冒号(:)。语句标号起标识语句的作用,与goto 语句配合使用。举个例子:

1
2
goto label;
cout << "This is the"

29 . extern关键字

1、extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:使用头文件,然后声明它们,然后其他文件去包含头文件;在其他文件中直接extern。

2、extern C作用

链接指示符extern C 如果程序员希望调用其他程序设计语言尤其是C 写的函数,那么调用函数时必须告诉编译器使用不同的要求,例如当这样的函数被调用时函数名或参数排列的顺序可能不同,无论是C++函数调用它还是用其他语言写的函数调用它,程序员用链接指示符告诉编译器该函数是用其他的程序设计语言编写的,链接指示符有两种形式既可以是单一语句形式也可以是复合语句形式。 // 单一语句形式的链接指示符 extern “C” void exit(int); // 复合语句形式的链接指示符 extern “C” { int printf( const char* … ); int scanf( const char* … ); } // 复合语句形式的链接指示符 extern “C” { #include } 链接指示符的第一种形式由关键字extern 后跟一个字符串常量以及一个普通的函数,声明构成虽然函数是用另外一种语言编写的但调用它仍然需要类型检查例如编译器会检查传递给函数exit()的实参的类型是否是int 或者能够隐式地转换成int 型,多个函数声明可以用花括号包含在链接指示符复合语句中,这是链接指示符的第二种形式花扩号被用作分割符表示链接指示符应用在哪些声明上在其他意义上该花括号被忽略,所以在花括号中声明的函数名对外是可见的就好像函数是在复合语句外声明的一样,例如在前面的例子中复合语句extern “C"表示函数printf()和scanf()是在C 语言中写的,函数因此这个声明的意义就如同printf()和scanf()是在extern “C"复合语句外面声明的一样,当复合语句链接指示符的括号中含有#include 时,在头文件中的函数声明都被假定是用链接指示符的程序设计语言所写的,在前面的例子中在头文件中声明的函数都是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
int main()
{
// 错误: 链接指示符不能出现在函数内
extern "C" double sqrt( double );
305 第七章函数
double getValue(); //ok
double result = sqrt ( getValue() );
//...
return 0;
}
如果把链接指示符移到函数体外程序编译将无错误
extern "C" double sqrt( double );
int main()
{
double getValue(); //ok
double result = sqrt ( getValue() );
//...
return 0;
}
    但是把链接指示符放在头文件中更合适,在那里函数声明描述了函数的接口所属,如果我们希望C++函数能够为C 程序所用又该怎么办呢我们也可以使用extern "C"链接指示符来使C++函数为C 程序可用例如。
// 函数calc() 可以被C 程序调用
extern "C" double calc( double dparm ) { /* ... */ }
    如果一个函数在同一文件中不只被声明一次则链接指示符可以出现在每个声明中它,也可以只出现在函数的第一次声明中,在这种情况下第二个及以后的声明都接受第一个声明中链接指示符指定的链接规则例如
// ---- myMath.h ----
extern "C" double calc( double );
// ---- myMath.C ----
// 在Math.h 中的calc() 的声明
#include "myMath.h"
// 定义了extern "C" calc() 函数
// calc() 可以从C 程序中被调用
double calc( double dparm ) { // ...
    在本节中我们只看到为C 语言提供的链接指示extern "C"extern "C"是惟一被保证由所有C++实现都支持的,每个编译器实现都可以为其环境下常用的语言提供其他链接指示例如extern "Ada"可以用来声明是用Ada 语言写的函数,extern "FORTRAN"用来声明是用FORTRAN 语言写的函数,等等因为其他的链接指示随着具体实现的不同而不同所以建议读者查看编译器的用户指南以获得其他链接指示符的进一步信息。

总结 extern “C” extern “C” 不但具有传统的声明外部变量的功能,还具有告知C++链接器使用C函数规范来链接的功能。 还具有告知C++编译器使用C规范来命名的功能。

30 . 动态内存管理

(动态内存管理)[https://blog.csdn.net/zgege/article/details/82054076]

31 .数组、链表、哈希、队列、栈数据结构特点,各自优点和缺点

数组(Array): 优点:查询快,通过索引直接查找;有序添加,添加速度快,允许重复; 缺点:在中间部位添加、删除比较复杂,大小固定,只能存储一种类型的数据; 如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。

链表(LinkedList): 优点:有序添加、增删改速度快,对于链表数据结构,增加和删除只要修改元素中的指针就可以了; 缺点:查询慢,如果要访问链表中一个元素,就需要从第一个元素开始查找; 如果应用需要经常插入和删除元素,就应该用链表。

栈(Stack): 优点:提供后进先出的存储方式,添加速度快,允许重复; 缺点:只能在一头操作数据,存取其他项很慢;

队列(Queue): 优点:提供先进先出的存储方式,添加速度快,允许重复; 缺点:只能在一头添加,另一头获取,存取其他项很慢;

哈希(Hash): 特点:散列表,不允许重复; 优点:如果关键字已知则存取速度极快; 缺点:如果不知道关键字则存取很慢,对存储空间使用不充分;

32. 友元函数

引入友元函数的原因
    类具有封装、继承、多态、信息隐藏的特性,只有类的成员函数才可以访问类的私有成员,非成员函数只能访问类的公有成员。为了使类的非成员函数访问类的成员,唯一的做法就是将成员定义为public,但这样做会破坏信息隐藏的特性。基于以上原因,引入友元函数解决。

(友元函数)[https://blog.csdn.net/qq_26337701/article/details/53996104]

33. 设计模式之单例模式、工厂模式、发布订阅模式以及观察者模式

(设计模式)[https://blog.csdn.net/m0_37322399/article/details/108515158]

34. 构造函数:

什么是构造函数?

通俗的讲,在类中,函数名和类名相同的函数称为构造函数。它的作用是在建立一个对象时,做某些初始化的工作(例如对数据赋予初值)。C++允许同名函数,也就允许在一个类中有多个构造函数。如果一个都没有,编译器将为该类产生一个默认的构造函数。

构造函数上惟一的语法限制是它不能指定返回类型,甚至void 也不行。

不带参数的构造函数:一般形式为 类名 对象名(){函数体}

带参数的构造函数:不带参数的构造函数,只能以固定不变的值初始化对象。带参数构造函数的初始化要灵活的多,通过传递给构造函数的参数,可以赋予对象不同的初始值。一般形式为:构造函数名(形参表);

创建对象使用时:类名 对象名(实参表);

构造函数参数的初始值:构造函数的参数可以有缺省值。当定义对象时,如果不给出参数,就自动把相应的缺省参数值赋给对象。一般形式为: 构造函数名(参数=缺省值,参数=缺省值,……); 析构函数:

当一个类的对象离开作用域时,析构函数将被调用(系统自动调用)。析构函数的名字和类名一样,不过要在前面加上 ~ 。对一个类来说,只能允许一个析构函数,析构函数不能有参数,并且也没有返回值。析构函数的作用是完成一个清理工作,如释放从堆中分配的内存。

一个类中可以有多个构造函数,但析构函数只能有一个。对象被析构的顺序,与其建立时的顺序相反,即后构造的对象先析构。 1、概念不同:

析构函数:对象所在的函数已调用完毕时,系统自动执行析构函数。

构造函数:是一种特殊的方法。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。 2、作用不同:

析构函数:析构函数被调用。


构造函数:为对象成员变量赋初始值 3、目的不同:

析构函数:”清理善后” 的工作

构造函数:主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。

35. C++模板

https://blog.csdn.net/zhaizhaizhaiaaa/article/details/104091658

36. C++ STL

https://www.cnblogs.com/shiyangxt/archive/2008/09/11/1289493.html

ref: https://blog.csdn.net/qq_52621551/article/details/122960158

c++ 八股文

关键字与运算符

1. 指针与引⽤

指针:存放某个对象的地址,其本⾝就是变量(命了名的对象),本⾝就有地址,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变 (地址可变,地址存储的值也可变)

引⽤:就是变量的别名,从⼀⽽终,不可变,必须初始化, 不存在指向空值的引⽤,但是存在指向空值的指针

2. const 关键字

const的作⽤:被它修饰的值不能改变,是只读变量。必须在定义的时候就给它赋初值。

顶层const: 表示指针本身是个常量 底层const: 表示指针所指的对象是一个常量

2.1 常量指针(底层const)(指针指的对象不可改变)

常量指针:是指定义了⼀个指针,这个指针指向⼀个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是指针对其所指对象的不可改变性。 特点:靠近变量名 形式:

  • const 数据类型 *指针变量 = 变量名
  • 数据类型 const *指针变量 = 变量名
  • 举例:
    • 1
      2
      3
      
        int temp = 10;
        const int* a = &temp;
        int const *a = &temp;

2.2 指针常量(顶层const)(指针不能改变) 指针常量:指针常量是指定义了⼀个指针,这个指针的值只能在定义时初始化,其他地⽅不能改变。指针常量强调的是指针的不可改变性。 特点: 靠近变量类型 形式: 数据类型 * const 指针变量=变量名

  • 实例:
    1
    
    int* const p = &temp

3. define 和 typedef的区别

ref : https://blog.csdn.net/CSSDCC/article/details/122049204 ref : https://zhuanlan.zhihu.com/p/513450251

Buy me a coffee~
支付宝
微信
0%