C/C++基础

文章目录

第一部分、基础知识点

一、C/C++基本概念

1、C&C++

1、 C++:是在C语言的基础上开发的一种面向对象编程语言。C++支持多种编程范式 --面向对象编程、泛型编程和过程化编程。
2、 C&C++的特点:
C语言特点

  • 作为一种面向过程的结构化语言,易于调试和维护;
  • 表现能力和处理能力极强,可以直接访问内存的物理地址;
  • C语言实现了对硬件的编程操作,也适合于应用软件的开发;
  • C语言还具有效率高,可移植性强等特点。

C++语言特点

  • 在C语言的基础上进行扩充和完善,使C++兼容了C语言的面向过程特点,又成为了一种面向对象的程序设计语言
  • 可以使用抽象数据类型进行基于对象的编程;
  • 可以使用多继承、多态进行面向对象的编程;
  • 可以担负起以模版为特征的泛型化编程。

3、C和C++区别:
C:结构化语言,是面向结构,重点在于算法和数据结构;
C++:面向对象,重点在于如何构建对象模型;

2、大端&小端

Big-Endian:类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
Little-Endian:将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

写程序判断大端小端:
int x=__;//给定一个x为十进制,int为4字节
byte a,b,c,d;byte k=(byte)&x;a=k[0],b=k[1],c=k[2],d=k[3];//定义4个byte类型,int型的x拆分成4个字节
//将a,b,c,d转换为16进制输出,查看是大端还是小端

3、在c++程序中调用被C编译器编译后的函数,extern“C”

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,
假设某个函数原型为:
void foo(int x, inty);
该函数被C编译器编译后在库中的名字为: _foo
而C++编译器则会产生像: _foo_int_int 之类的名字。
为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

二、预处理&修饰符

1、宏定义

宏定义是替换,不做计算,也不做表达式求解
不存在类型,也没有类型转换,没有参数类型
宏展开不占运行时间,只占编译时间,函数调用占运行时间
函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存,变量定义分配内存
①不带参数:宏替换
#define Pi 3.1415926
·提供程序可读性、易读性,减少不一致性,减少输入错误,便于修改
②带参数:参数替换
#define MIN(A,B) ((A)<(B)?(A):(B))
·实参如果是表达式容易出问题
·宏名和参数的括号间不能有空格
Tips:
a. #define不能以分号结束,注意括号使用(把参数括起来)
b. 预处理器会计算常数表达式的值,可以define中包含函数表达式
c. 整型数会溢出要告诉编译器用UL

2、Const

限定一个变量不允许被改变,产生静态作用。使用const在一定程度上可以提高程序的安全性和可靠性。
(1)可以定义const常量,具有不可变性。
  例如:const int Max=100; Max++会产生错误;
(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。例如: void f(const int i) { …} 编译器就会知道i是一个常量,不允许修改;
(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
(5)可以节省空间,避免不必要的内存分配。 例如:
(6)提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

const的主要用途
① 定义只读变量,即常量 ,不能被修改
② 修饰函数的参数和函数的返回值 :
a. const修饰函数:承诺在本函数内部不会修改类内的数据成员,不会调用其它非const成员函数。
b. const构成函数重载:const对象只能调用const 函数,非const对象优先调用非 const 数。
c. const 函数只能调用 const 函数。非 const 函数可以调用 const 函数。
③ 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值

Const修饰指针(4种情况)
const 位于左侧:修饰指针所指向的变量,与变量声明符的位置无关
位于
右侧:修饰指针本身,即指针本身是常量
Int b=500;
① const inta=&b;//在左侧,修饰指针所指变量,即指针所指内容为常量,不允许对 内容进行更改;指针可以指向别的地方,可以通过改变b来改变内容, 但不能a没有权力进行更改
② int const a=&b;//同上
③ int
const a=&b; //在
右侧,指针本身是常量,不可以对指针本身进行操作,而指针 指向的内容不是常量,a不能指向别的地方
④ const int
const a=&b;//指针本身和指向的内容均为常量

const修饰函数
①const在函数之后:Int point::gety() const{}
成员函数不改变类的数据成员,即“只读”函数,提高程序可读性、可靠性,若修改 变量或调用非const成员函数编译报错
②const在函数前,函数的返回值是常量

Const和#define区别
①数据类型:const有数据类型,编译器有安全检查;宏没有,且不进行安全检查
②const可以进行调试,宏不行

3、Sizeof

(1)基本数据类型长度

charshortIntFloatdoublelonglonglong*Unsigned xxxBool
16位122442
32位12448484同xxx1
64位12448888同xxx1

1字节数=8位

(2)sizeof和strlen的区别

  • Sizeof:运算符,可以用类型或函数做参数,头文件中typedef为unsigned int;它 的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
    Strlen:函数,只可以做char*的参数,必须是以\0结尾。strlen要在运行时才能计 算。当数组名作为参数传入时,实际上数组就退化成指针了。它的功能是: 返回字符串的长度。实际完成的功能是从代表该字符串的第一个地址开始 遍历,直到遇到结束符NULL。返回的长度大小不包括NULL。
  • sizeof求的是字节长度/ strlen求的却是实际长度。
  • sizeof求的是分配过来的长度/strlen求的是实际使用的长度,strlen以’\0’结束的。
  • sizeof是在编译是计算的,而strlen是在运行是计算的。
  • sizeof参数可以是数组、指针、类型、对象、函数等。由于在编译时计算,因此sizeof 不能用来返回动态分配的内存空间的大小。sizeof返回的值表示的含义如下:
    数组——编译时分配的数组空间大小;
    指针——存储该指针所用的空间大小(是长整型,应该为4);
    类型——该类型所占的空间大小;
    对象——对象的实际占用空间大小;
    函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。

4、内联函数

内联函数:用inline关键字修饰的函数。是在编译时将函数体嵌入在每一个调用处,不是跳转,增加空间小号来换取效率提高。编译时,类似宏替换,使用函数体替换调用处的函数名。内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益

内联函数适用于(2种)
a. 一个函数不断被重复调用
b. 函数只有简单几行,不含有for、while、switch

内联函数缺点
a. 以代码复制为代价,省去函数调用开销
b. 要复制代码,使得代码总量增大,消耗内存空间
c. 不适用于:代码过程,使用内联导致内存消耗过大/函数体内出现循环,内联比函数调用开销大

内联函数vs宏的区别
① 内联函数:编译时直接嵌入到目标代码中/ 宏:在代码处不加任何验证的简单替换
② 参数检查:内联函数要做参数类型检查/宏:不检查
③ 内联函数:函数定义体和inline放在一起构成/宏:不是函数,是编译前将程序中 有关字符串替换成宏体

5、头文件中的ifndef/define/endif 是干什么用的

  防止头文件被重复包含

6、static关键字(静态函数&静态变量)

① 函数体内的static:作用域为该函数体,该变量内存只被分配一次,其值在下次调用仍维持上次的值
② 模块内static变量:全局变量,可以被模块内所有函数访问,但不能被模块外其他函数访问,使用范围被限制再声明它的模块内
③ 模块内的static函数:只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内
④ 类中static变量:属于整个类所拥有,对类的所有对象只有一个复制
⑤ 类中static函数:属于整个类所拥有,不接受this指针,因而只能访问类中的static变量

Q:类的static变量在什么时候初始化?函数的static变量在什么时候初始化?
类的静态成员变量在类实例化之前就已经存在了,并且分配了内存。
函数的static变量在执行此函数时进行初始化。

7、静态函数&静态变量?

(1)类静态数据成员“在编译时创建并初始化:在该类的任何对象建立之前就存在,不属于任何对象,而非静态类成员变量则是属于对象所有的。类静态数据成员只有一个拷贝,为所有此类的对象所共享。
(2)类静态成员函数属于整个类,不属于某个对象,由该类所有对象共享。
1、static 成员变量实现了同类对象间信息共享。
2、static 成员类外存储,求类大小,并不包含在内。
3、static 成员是命名空间属于类的全局变量,存储在 data 区的rw段。
4、static 成员只能类外初始化。
5、可以通过类名访问(无对象生成时亦可),也可以通过对象访问。
(3)静态成员函数的意义:不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时 this指针时被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。

三、指针

1、指针&引用区别(4点)

① 非空:指针可以为空;引用不能使用指向空值的引用
② 合法性:指针使用要检测是否为空;引用不需要检测合法性
③ 可修改:指针可以修改指向的对象;引用不可以修改
④ 应用:指针:存在不指向任何对象可能/不同时刻指向不同对象
引用:总指向一个对象并且不会改变指向
补:
⑤ 引用是变量的一个别名,内部实现是只读指针
⑥ 引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
⑦ 引用变量内存单元保存的是被引用变量的地址
⑧ “sizeof 引用" = 指向变量的大小 , “sizeof 指针”= 指针本身的大小
⑨ 引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
⑩ 引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址

2、指针数组&数组指针

指针数组:一个数组里装着指针
数组指针:一个指向数组的指针

3、new/delete&malloc/free

用于申请动态内存和释放内存
①操作对象不同:new/delete是C++的运算符;malloc/free是C/C++的标准库函数,需要库函数支持;
②空间分配:new自动计算需要分配的空间,new 内置了sizeof、类型转换和类型安全检查功能,完成了部分初始化工作;而malloc需要手工计算字节数,用malloc 申请一块长度为length 的整数类型的内存
③类型安全:new是类型安全的,而malloc不是

补: C++中有了malloc / free , 为什么还需要 new / delete?
1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

四、 C++11 新特性

新特性用途示例
关键字(用于类型推导)auto自动推导类型,还不能用于推导数组类型auto i = 5;
decltypeauto 关键字只能对变量进行类型推导的缺陷而出现的,分析表达式并得到它的类型,却不实际计算表达式的值decltype(x+y)
拖尾返回类型拖尾返回类型(trailing return type),利用 auto 关键字将返回类型后置
循环基于范围的for循环使循环更简洁for(auto &i : arr)
类型char16_t
char32_t
long long int
编译constexpr
说明符default
delete
override
final
委托构造函数delegating constructors
初始化std::initializer_list初始化列表允许构造函数或其他函数像参数一样使用初始化列表,为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁
uniform initialization统一初始化
指针nullptr 空指针代替NULL,区分空指针和0
智能指针

1、指针、智能指针(nullptr、shared_ptr、std::weak_ptr)

(1)nullptr
  • 作用: C++11 引入了 nullptr 关键字,专门用来区分空指针、0
  • 原有问题:传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL
  • 实现:nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。当需要使用 NULL 时候,养成直接使用 nullptr的习惯
(2)智能指针 shared_ptr、unique_ptr、weak_ptr

shared_ptr
多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的
  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。std::weak_ptr:与std::shared_ptr最大的差别是在赋值是,不会引起智能指针计数增加.shared_ptr会导致相互引用:导致的问题就是释放条件的冲突,最终也可能导致内存泄漏。用weak_ptr解决

unique_ptr
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr

2、类型推导(auto、decltype)

(1)auto

auto新的语意实现自动类型推导,其推导阶段在编译期实现,而且由于编译期间需要判断左右值是否匹配,所以不会对编译和运行速度带来影响.使用auto的时候,编译器根据上下文情况,确定auto变量的真正类型
ps:auto作为函数返回值时,只能用于定义函数,不能用于声明函数。

(2)decltype

decltype()可以在编译期间获取变量的类型.auto能够让你声明一个变量。而decltype则能够从一个变量或表达式中得到类型

(3)拖尾返回类型

利用 auto 关键字将返回类型后置

decltype(x+y) add(T x, U y);

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}

3、类特性修改

(1)默认函数行为(dafault、delete)

在没有指定的情况下,c++会对类设置默认的构造函数、拷贝构造函数、赋值函数以及析构函数,但是有时候我们并不需要这些默认函数,因此在C++11中引入了对这些特性进行精确控制的特性:
default指定生成默认函数,
delete指定禁用默认函数。如果禁用了默认的构造函数和析构函数,必须指定一个自定义的函数。

class Test {
public:
    Test() = default;       //指定为Test类生成默认构造函数,如果设置为delete,就是禁用默认构造函数,如果禁用了
    ~Test() = default;      //默认析构函数
    Test(const Test&) = delete;    //禁用拷贝构造函数
    Test& operator=(const Test&) = delete;  //禁用类赋值函数
};

Test a;
Test b(a);      //error,因为已经被禁用
Test c = a;     //error,因为已经被禁用
(2)构造函数(委托、继承构造函数using)

C++11提供了两种新的构造函数特性,用于提升类构造的效率,分别是委托构造和继承构造,前者主要用于多构造函数的情况,而后者用在类继承方面.
(1)委托构造函数
委托构造的本质为了简化函数代码,做到复用其他构造函数代码的目的。用于多构造函数情况

(2)继承构造函数(using关键字)
c++在继承的时候,需要将构造函数的参数逐个传递到积父类的构造函数中完成父类的构造,这种效率是很低下的,因此c++11引入了继承构造的特性,使用using关键字.用于类继承方面

(3)显示控制虚函数重载(override、final)

由于虚函数的特性,可能会被意外进行重写,为了做到精确对虚函数重载的控制,c++11使用了override和final关键字完成对这一特性的实现.
override关键字 : 显式声明对虚函数进行重载
final关键字 : 显式终结类的继承和虚函数的重载使用

4、STL容器

(1)std::array

std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可. std::array<int, 4> arr= {1,2,3,4};

(2)std::forward_list

std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率

(3)无序容器unordered_map、unordered_set

C++11 引入了两组无序容器,无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。

std::unordered_map/std::unordered_multimap
td::map使用的数据结构为二叉树,而std::unordered_map内部是哈希表的实现方式,哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。

std::unordered_set/std::unordered_multiset
std::unordered_set的数据存储结构也是哈希表的方式结构,除此之外,std::unordered_set在插入时不会自动排序,这都是std::set表现不同的地方

(4)元组std::tuple

tuple是一个固定大小的不同类型值的集合,是泛化的std::pair,其中的元素个数不再限于两个,而且功能更加丰富.
元组的使用有三个核心的函数:
std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
std::tie: 元组拆包
std::tuple_cat:合并两个元组,可以通过 std::tuple_cat 来实现

5、多线程

(1)std::thread

std::thread为C++11的线程类,使用方法和boost接口一样,非常方便,同时,C++11的std::thread解决了boost::thread中构成参数限制的问题,我想着都是得益于C++11的可变参数的设计风格。

(2)std::atomic

std::atomic为C++11分装的原子数据类型.
从功能上看,简单地说,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。

(3)std::condition_variable

std::condition_variable就像Linux下使用pthread_cond_wait和pthread_cond_signal一样,可以让线程休眠,直到被唤醒,现在在从新执行。线程等待在多线程编程中使用非常频繁,经常需要等待一些异步执行的条件的返回结果。

std::condition_variable cv;
while (!ready) cv.wait(lck); //调用cv.wait(lck)的时候,线程将进入休眠
cv.notify_all(); //休眠结束

6、其他

(1)for循环(区间迭代)
  • 作用: for循环语句更为简洁
  • 原有问题:循环语句麻烦
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i){
   xxx
}
  • 优化后:
for(auto &i : arr) {    
    std::cout << i << std::endl;
}
(2)匿名函数 lamda表达式

lambda 表达式:实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的. [ caputrue ] ( params ) opt -> ret { body; }; lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

  1. [] 不捕获任何变量。
  2. [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  3. [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
  4. [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
  5. [bar]按值捕获bar变量,同时不捕获其他变量。
  6. [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
(3)初始化列表std::initializer_list

统一初始化语言提供了统一的语法来初始化任意的对象,用大括号来统一初始化
std::initializer_list还把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁

C++11新特性参考:https://blog.csdn.net/jiange_zh/article/details/79356417

(4)正则表达式

正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:

  1. 检查一个串是否包含某种形式的子串;
  2. 将匹配的子串替换;
  3. 从某个串中取出符合条件的子串。
    C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。
    std::regex_match 用于匹配字符串和正则表达式,有很多不同的重载形式
    std::smatch 被定义为了 std::match_results,也就是一个子串迭代器类型的 match_results。使用 std::smatch 可以方便的对匹配的结果进行获取

五、面向对象

1、面向过程&面向对象

面向过程:分析解决问题所需要的步骤,然后把这些步骤依次实现
面向对象:把构成问题的事务分解成各个对象,建立对象描述某个事物在整个解决问题的步骤中的行为

2、面向对象的三大特征——封装、继承、多态

  • 封装:封装就是将数据或函数等集合在一个个的单元中,也就是将对象的属性和行为封装起,成类,对客户隐藏其实现细节,意义在于保护或者防止代码(数据)被我们无意中破坏
  • 继承:属性和行为相似,父类&子类。继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
  • 多态(虚函数):表现出多种形态,具有多种实现方式。同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
    C++的多态性用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
    1)用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数;
    2)存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的;
    3)虚函数主要用于结合动态绑定来实现多态,多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
    4)纯虚函数是虚函数再加上 = 0;
    5)抽象类是指包括至少一个纯虚函数的类;纯虚函数:virtual void fun()=0;即抽象类,必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
  • 类:把一些具有共性的对象归类后形成一个集合,也就是所谓的类。

3、构造函数&析构函数

构造函数:在对象类中定义或声明与类同名、无返回值类型的成员函数;创建对象时,会被自动调用;
拷贝构造函数:创建一个对象时,用同类的另一个对象对其进行初始化
析构函数:当对象消亡时,在系统收回它所占的存储空间之前,会自动调用析构函数,用来归还申请的系统资源

4、虚函数

(1) 虚函数定义
① 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数;
② 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的;
③ 虚函数主要用于结合动态绑定来实现多态,多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。

(2)虚函数表构造过程:
在这里插入图片描述
(3)虚函数调用过程

在这里插入图片描述
(4)纯虚函数:

  1. 定义:
    ① 纯虚函数是虚函数再加上 = 0;
    ② 将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

  2. 在什么情况下使用纯虚函数(pure vitrual function)?
    当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;这个方法必须在派生类(derived class)中被实现;

  3. 虚函数vs纯虚函数的区别
    虚函数为了重载和多态。 在基类中是有定义的,即便定义为空。 在子类中可以重写。
    纯虚函数在基类中没有定义, 必须在子类中加以实现。
    子类继承父类大部分的资源,不能继承的有构造函数,析构函数,拷贝构造函数, operator=函数,友元函数等等

5、什么时候要用虚析构函数?

通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚的。否则其删除效果将无法实现。一般情况下,这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,从而千万内存泄漏。
原因:在公有继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员。如果想要用基类对非继承成员进行操作,则要把基类的这个操作(函数)定义为虚函数。那么,析构函数自然也应该如此:如果它想析构子类中的重新定义或新的成员及对象,当然也应该声明为虚的。

6、c++怎样让返回对象的函数不调用拷贝构造函数

拷贝构造函数前加 “explicit” 关键字

7、结构&联合

(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

8、重载和覆盖的区别是什么?

1) 方法的覆盖是子类和父类之间的关系,是垂直关系。
方法的重载是同一个类中方法之间的关系,是水平关系。
2) 覆盖只能由一个方法,或只能由一对方法产生关系;
方法的重载是多个方法之间的关系。
3) 覆盖要求参数列表相同
重载要求参数列表不同。
4) 覆盖关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定; 重载关系是根据调用时的实参表与形参表来选择方法体的。

六、STL

STL介绍

STL包括:容器、算法、迭代器

容器:即存放数据的地方。在STL中,容器分为两类:序列式容器和关联式容器。

  • 序列式容器,其中的元素不一定有序,但都可以被排序。如:vector、list、deque、stack、queue、heap、priority_queue、slist;
  • 关联式容器,内部结构基本上是一颗平衡二叉树。所谓关联,指每个元素都有一个键值和一个实值,元素按照一定的规则存放。如:RB-tree、set、map、multiset、multimap、hashtable、hash_set、hash_map、hash_multiset、hash_multimap。
    下面各选取一个作为说明。
    vector:它是一个动态分配存储空间的容器。区别于c++中的array,array分配的空间是静态的,分配之后不能被改变,而vector会自动重分配(扩展)空间。
    set:其内部元素会根据元素的键值自动被排序。区别于map,它的键值就是实值,而map可以同时拥有不同的键值和实值。

算法:如排序,复制……以及个容器特定的算法。
迭代器:一种智能指针.迭代器提供了一种方法,使它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构。它将容器和算法分开,好让这二者独立设计。

STL 对比

13个头文件

数据结构描述实现应用场景使用详解
<vector>向量连续存储的元素可变数组数组vector详解
<list>列表在常规时间内,在序列已知的任何位置插入或删除元素,无法通过位置来直接访问序列中的元素环状的双向链表
<forward_list>以单链表的形式存储元素,与list区别它不能反向遍历元素;只能从头到尾遍历
<queue>队列包含先入先出的事务处理时队列
<deque>双队列双向开口的连续线性空间,可以在容器的头部和尾部高效地添加或删除对象双端队列
<priority_queue>会对元素排序,从而保证最大元素总在队列最前面的队列用最大堆实现,用 vector 存储
<queue>优先队列
<set>集合由节点组成的红黑树,不同元素不能拥有相同次序、键值相当,默认生序排列红黑树,自动排序
<multiset>多重集合循序存在两个次序相等的元素的集合
<map>映射有键-值对组成
<multimap>多重映射允许键对有相同的次序
<stack>后入先出(Last-In-First-Out,LIFO)的压入栈
<iterator>
<memory>
<numeric>
<algorithm>
<functional>
<utility>

七、C++线程管理

1、并发&并行

并发: 并发指的是两个或多个独立的活动在同一时段内发生。同一时间段内可以交替处理多个操作
并行: 是指两个或多个独立的操作同时进行。同一时刻内同时处理多个操作.在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。

多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

多线程并发
多线程并发指的是在同一个进程中执行多个线程。线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

2、多线程

C++11 新标准中引入了几个头文件来支持多线程编程:
< thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在中声明.
< atomic > :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
< mutex > :包含了与互斥量相关的类以及其他类型和函数
< future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
< condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。

线程创建:std::thread(do_task);
线程执行,等待线程结束(detach、join):
detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。而使用detach方式则不会对当前代码造成影响,当前代码继续向下执行,创建的新线程同时并发执行,
join方式,等待启动的线程完成,才会继续往下执行。当使用join方式时,会阻塞当前代码,等待线程完成退出后,才会继续向下执行
向线程函数传递参数:需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用std::ref进行转换
movable 传递:thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。可以在函数内部或者外部进行传递
线程id:每个线程都一个标识,可以调用get_id获取。

第二部分 操作系统

1、进程&线程

进程:执行着的应用程序,不同的进程使用不同的内存空间
线程:进程内部的一个执行序列。一个进程可以有多个线程,而所有的线程共享一片相同的内存空间,线程又叫做轻量级进程。

2、线程的状态(5个)

  1. 新建状态:使用 new /Thread类/其子类建立一个线程对象后,处于新建状态。
  2. 就绪状态:调用了start()方法,线程进入就绪状态。处于就绪队列中等待调度就绪状态:调用了start()方法,线程进入就绪状态。处于就绪队列中等待调度
  3. 运行状态:获取CPU 资源,就可以执行run(),此时线程便处于运行状态。可以变为阻塞状态、就绪状态和死亡状态运行状态:获取CPU 资源,就可以执行run(),此时线程便处于运行状态。可以变为阻塞状态、就绪状态和死亡状态
  4. 阻塞状态:一个线程执行了sleep(睡眠)/suspend(挂起)等方法,失去所占用资源之后,进入阻塞状态。在睡眠时间到/获得资源后可以重新进入就绪状态。阻塞状态:一个线程执行了sleep(睡眠)/suspend(挂起)等方法,失去所占用资源之后,进入阻塞状态。在睡眠时间到/获得资源后可以重新进入就绪状态。
    分为三种:
    a. 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    b. 同步阻塞:线程在获取synchronized同步锁失败(因为同步锁被其他线程占用)。
    c. 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  5. 死亡状态: 线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

3、阻塞状态的三种情况:

a. 位于对象等待池中的阻塞:执行了wait()
b. 位于对象锁中的阻塞:某对象的同步锁被其他线程占用,JVM把这个线程放入这个对象的锁池中
c. 其他的阻塞状态:执行了sleep()/调用其他线程的join()/发出I/O请求

4、创建线程(三种方法)

① 通过实现 Runnable 接口;
② 通过继承 Thread 类本身;
③ 通过 Callable 和 Future 创建线程。

创建线程的三种方式的对比:
a. 实现Runnable、Callable接口的方式创建多线程时,线程类还可以继承其他类。
b. 继承Thread 类的方式创建多线程时,编写简单,访问当前线程直接使用this即可
c. Callable的call()可以返回值和抛出异常,而Runnable的run()没有这些功能。Callable可以返回装载有计算结果的Future对象。

5、停止线程的方法(3种)

① 使用退出标志,run()方法结束之后
② 使用thread的interrupt()
③ 使用thread的stop()强行终止

6、sleep()\wait()\yield()区别

 Sleep() & yield():
sleep():让当前正在执行的线程休眠
yield():暂停当前正在执行的线程对象,并执行其他线程,交出CPU使用时间
① sleep会给其他线程运行的机会,而不考虑优先级,yield只给同优先级或者更高的机会
② 线程执行sleep后跳转到阻塞,而yield跳转到就绪
③ sleep抛出InterruptedException异常,而yield没有抛出异常
④ sleep有更好的移植性

 Sleep() & wait()
① Sleep:是thread类的方法,会使线程暂停执行指定时间而将机会让给其他线程,但监控任然保持,暂停后会自动恢复,不会释放对象锁
② Wait:是Objeck类的方法,调用wait会导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有重新获得对象锁才能进入运行状态

7、进程&线程间的通信方式

(1)进程间通信方式:
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
信号量( semophore ): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存( shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

(2)线程间通信方式:

  • 全局变量;
  • Messages消息机制;
  • CEvent对象(MFC中的一种线程通信对象,通过其触发状态的改变实现同步与通信)。

8、线程同步的方式

 Linux:   互斥锁、条件变量和信号量

9、同步&异步IO

A. 同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
B. 异步:当一个异步过程调用发出后,调用者不会立刻得到结果。实际处理这个调用的部件是在调用发出后,通过状态、通知来通知调用者,或通过回调函数处理这个调用。

同步通信和异步通信的区别?
答:
1) 同步通信要求接收端时钟频率和发送端时钟频率一致,发送端发送连续的比特流;异步通信时不要求接收端时钟和发送端时钟同步,发送端发送完一个字节后,可经过任意长的时间间隔再发送下一个字节。
2) 同步通信效率高;异步通信效率低。
3) 同步通信较复杂,双方时钟的允许误差较小;异步通信简单,双方时钟可允许一定误差。

10、协程

(1)定义:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置;线程是抢占式,而协程是协作式;
(2)协程的优点:跨平台跨体系架构无需线程上下文切换的开销无需原子操作锁定及同步的开销方便切换控制流,简化编程模型高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。协程的缺点:
(3)无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU;进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序:这一点和事件驱动一样,可以使用异步IO操作来解决。

11、虚拟内存的概念与介绍

(1)虚拟内存:允许将一个作业分多次调入内存,需要时就调入,不需要的就先放在外存。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:#请求分页存储管理#请求分段存储管理#请求段页式存储管理

(2)虚拟内存的意义:
① 虚拟内存可以使得物理内存更加高效。虚拟内存使用置换方式,需要的页就置换进来,不需要的置换出去,使得内存中只保存了需要的页,提高了利用率,也避免了不必要的写入与擦除;
② 使用虚拟地址可以使内存的管理更加便捷。在程序编译的时候就会生成虚拟地址,该虚拟地址并不是对应一个物理地址,使得也就极大地减少了地址被占用的冲突,减少管理难度;
③ 为了安全性的考虑。在使用虚拟地址的时候,暴露给程序员永远都是虚拟地址,而具体的物理地址在哪里,这个只有系统才了解。这样就提
高了系统的封装性。

第三部分 网络

第四部分 数据结构

一、hash表

1、hash冲突解决

(1)开放定址法:再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
线性探测再散列:突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
二次探测再散列:冲突发生时,在表的左右进行跳跃式探测,比较灵活
伪随机探测再散列:建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

(2)再哈希法:同时构造多个不同的哈希函数,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

(3)链地址法(拉链法):这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

(4)建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值