C++ 基础梳理不完整总结

从上一次博客总结到现在,中间已经跨越了半年不止的时间,其实应该从C语言开始总结比较好,但由于一些外部因素,还是先从C++开始总结,这次总结会提到部分书上所展示的代码或是讲解,毕竟人家还是有很多精华的地方需要学习,我这仅仅几年的时间去精炼某些知识点还是有点勉强的,最终可能导致只有我自己看得懂,所以这次的全面总结会依附书籍来进行(针对个人来说,当然不可能完全的全面),此外,对于一些较为常见的部分暂时会仅仅提及,并不展开讲解,日后若是有机会再补充。

 

一.C++相关概念和内置数据类型

Ⅰ.概念

>1.C++中,执行动作被称为表达式,以分号结尾的表达式被称为语句,语句是C++中最小的程序单元。

>2.在C++中,每个程序必须包含一个被称作main()的函数,由程序员提供,并且只有此函数存在程序才能运行。 

Ⅱ. 基本内置类型

C++基本内置类型包括算术类型和空类型(void)

>1.算术类型

  算术类型分为整型(包括字符和布尔类型)和浮点型

  单个算术类型数据所占的比特数在不同机器上是有差别的,下图是C++标准规定的所占比特数的最小值(其中布尔类型(bool)的取值为真(true)或假(false)):

  ps.编译器被允许赋予这些类型更大的尺寸

  ①C++提供的字符类型中,基本字符类型是char,一个char的空间应该确保大小和一个机器字节一样。

  ②C++规定一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long(C++11提供)至少和一个long一样大。

  ③除去布尔型和扩展的字符型外,其他整型可划分为带符号与无符号,一般类型都是带符号类型,如int,short,long等,类型名前添加signed可得到当前类型的无符号类型

  ④关于三种字符型(char,unsigned char,signed char),《primer C++》中如此描述:

  

  ps. unsigned char 理论取值范围是 0 到 255

  ⑤基本类型转换:

  

  ps.1.类型所能表示的值的范围决定了转换的过程

       2.避免无法预知和依赖于实现环境的行为(不仅指类型转换)

Ⅱ.变量

>1.定义不是初始化、初始化不是赋值,未初始化变量是一个不确定的值,可能会引发运行时故障。

>2.C++支持分离式编译,使用关键字 extern 来声明且不定义变量

extern int a;   //仅声明而非定义
int a;   //声明且定义

  ps.变量只能被定义一次,但可以多次声明;试图初始化extern标记的变量会引发错误。

>3.标识符与变量命名规则

>4.变量作用域

>5.引用和指针的区别

Ⅲ.指针  

>1.void*指针

  void*是一种特殊的指针类型,可用于存放任意对象的地址,但无法直接操作所指向的对象,因为对象不明确。但可以通过static_cast找回存在于void*指针中的值:

void* p = &a;  
int* local = static_cast<int*>(p);

ps.把指针存放在void*中,然后通过static_cast强行转换后,应该确保指针的值不变,也就是保证操作前后类型对应,若是强转后类型不符,会产生未定义的后果。

>2.多级指针 ,按照逻辑理解,根据*的个数进行相应次数的解引用。

Ⅳ.const限定符

const限定符定义的变量的值无法被改变,任何试图对其赋值的操作都会引发出错,所以必须需要初始化才能使用。

>1.const对象默认只在文件内有效,若是需要在其他文件中声明使用当前文件中定义的const变量:

>2.常量指针与指针常量的区别(eg: const int *p(与int const *p同义,const对于类型透明) 和 int* const p),指向常量的指针常量:const int* const p = &a;

>3.类型别名方式

  ①使用关键字 typedef   eg: typedef int onetype

  ②C++11提供了 别名声明 方式来定义类型别名 eg:using onetype = int; (属于语句操作)

>4.自定义数据结构(struct)

 

二.string、vector、数组

Ⅰ.标准库类型string

头文件 #inclue <string> 

            using namespace std;

>1.定义,初始化方式:

>2.string对象读取标准输入时,会忽略开头的空格,从第一个真正字符开始读取到下一个空格。

>3.getline() 读取整行时使用   

string str; 
getline(cin,str);

>4.string对象的内置接口使用

>5.string对象之间的计算(相加,赋值等)

Ⅱ. 标准库类型vector

vector是一种容器,且是一种模板而非类型,其中存储的元素类型与vector生成的类型一致,如vector<int>中存储的都为int数据

>1.初始化

>2.vector成员函数使用

>3.vector支持[ ]元素引用,同数组一样,第一个元素下标为0

Ⅲ.迭代器初识(后续章节深入)

>1.string和vector除了可以使用[ ]下标元素引用之外,也可以通过迭代器访问其中的元素

>2.除了vector之外其他的所有标准库容器也都可以使用迭代器。

>3.迭代器的使用

  ①容器通过名为 begin( ) 和 end( ) 的成员函数还返回指向第一个元素和最后一个元素的下一个位置的迭代器;同时,若是容器为空,begin()和end()返回的是同一个迭代器(都为end()返回的迭代器类型)

  ②C++11引入了cbegin() 和 cend() ,与之前的成员函数不同的是,cbegin和cend返回的迭代器类型都是const_iterator

>4.迭代器的运算符使用

 ps.解引用一个非法迭代器或是尾迭代器(end())都会引发未定义错误

①迭代器可以通过 + 一个数或是 - 一个数 来得到指向位置向前移动或向后移动若干位置的元素的迭代器(同时支持+=和-=的复合赋值语句)。

②两个迭代器相减可以得到他们所指向的元素之间的距离。

③迭代器可以使用>= ,> ,<= ,< 关系运算符,大小代表他们所指向的元素在容器中的位置前后(位置靠前小于位置靠后)

>5.迭代器的类型

vector<int> v1;
auto b1 = v1.begin();
auto e1 = v1.end();
//b1和e1的类型为vector<int>::iterator

const vector<int> v2;
auto b2 = v2.begin();
auto e2 = v2.end();
//b2和e2的类型为vector<int>::const_iterator

Ⅳ.数组

C++中,数组是与vector类似的数据结构,不同的是应用场景。

>1.数组的大小是确定不变的(初始化后),不能随意添加元素,灵活性不如vector,但合适的场景下运行性能较好。

>2.数组的初始化(显式或隐式)

>3.数组之间不允许直接拷贝和赋值

int a[] = {1,1,1};
int b[] = {0};
int c[] = a; //错误
b = a; //错误

>4.多维数组的定义(数组的数组)

  ①初始化

  ②下标引用

>5.指针和数组

两者的关系与C语言中类似。C++中,指针和数组较为紧密,使用数组的时候编译器一般都会将他转换为指针。

  ①对数组元素使用取地址符(&)就可以获得指向该元素的指针。

  ②在用到数组名的地方,编译器都会自动将其转换为一个指向数组首元素的指针。

  ③与容器类似,数组可以通过begin(数组名)或是end(数组名)的方式得到数组首指针或是尾后指针。可以通过此特性来初始化vector

int a[] = {1,2,3,4,5};
vector<int> v1(begin(a), end(a));
vector<int> v2(a+2, a+4);  //也可以只用一部分元素进行初始化,这里用了a[2],a[3],a[4]

>6.指针和多维数组

  使用多维数组名时,也会自动转换为指向数组首元素的指针。实际是指向第一个内存数组的指针。

>7.函数指针

 

三.C++函数

Ⅰ.函数编写、函数调用

>1.实参与形参

>2.参数列表

  ①使用引用类型参数避免拷贝(对于引用的操作是作用在引用所引的对象上的)。

  ②数组做形参时,会被转换为指针。当传递参数是指针时,实际是传递一个指向数组首元素的指针。

  ③initializer_list的使用(C++11提供的标准库类型)

  如果参数的实参数量未知,但是全部实参的类型都相同,就可以使用initializer_list类型的形参,使用方式类似于vector,不同的是initializer_list中的元素永远是常量值,不可被改变

void func(initializer_list<int> li)
{
    for(auto sta = li.begin() ; sta != li.end() ; sta++)
    {
         cout<< *sta <<" ";
    }
    cout<<endl;
}

>3.缺省参数

>4.函数声明

Ⅱ.局部对象,局部静态变量

  ①形参和函数体内部定义的变量统称为局部变量,仅在定义位置函数的作用域内可见,局部变量同时会隐藏外部所有同名变量声明。

  ②将局部变量定义前加static可获得局部静态变量定义,局部静态变量在第一次执行时初始化,知道程序终止才会被终止。(一般用来统计次数)

Ⅲ.函数重载

处于同一作用域内的相同函数名不同函数参数列表的函数定义称之为函数重载。

>1.调用时会根据传入的参数调用对应的重载函数

>2.加const定义无法定义为函数重载

void func(int a);
void func(const int a);   //属于重复声明错误

 ps.两种声明是等价的

Ⅳ.内联函数

在函数声明前加上 inline 会将其声明为内联函数。内联函数可避免函数调用的开销,通常是将他在每个调用点上之间把函数展开执行。(内联说明只是一个给编译器的请求,编译器不一定执行)

 

四.C++中的类

类的基本思想是数据抽象和封装 -- 《C++primer》

数据抽象通过接口和实现来完成; 封装体现了类的接口和实现的分离。

Ⅰ.定义抽象数据类型

>1.类作用域和成员函数声明,成员函数的实现

>2.构造函数

  每个类都定义了它的对象被初始化的方式,类通过构造函数来控制其对象的初始化过程。

  ①构造函数名和类名相同,没有返回值类型,有参数列表(可为空)和函数体(可为空)

  ②类可包含多个构造函数(支持构造函数的函数重载)

  ③默认构造函数不是所有类都能正常使用的

  ④类数据成员的初始化顺序和他们在类中定义的出现顺序一样;不会受到初始化列表中顺序的影响。

>3.访问控制

  不同访问限制的定义:public(整个程序内可被访问)、protected(可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问)、private(只可被类成员函数访问)

>4.class和struct关键字不同的默认访问权限(class-private,struct-public)

>5.友元

友元能且只能通过它所在类的对象访问它所在类的成员,只需要在声明前面加上friend关键字即可。

class A
{
    ……
    friend class B;   //友元类声明,类B为类A的友元类
    ……
}
----------------------------------------------
class A
{
    ……
    friend int B::Func(……);   //友元函数声明 
    ……
}
    

 ps.友元声明仅仅是指定访问权限,而不是一个通常意义的声明,若是希望类对象能调用某个友元函数,则必须在友元声明之外再专门对函数进行一次声明。

>6.类的其他特性

  ①类中常有一些规模较小的函数适合被声明为内联函数。

  ②类的成员函数和非成员函数都支持函数重载

  ③静态成员函数可以在类内部或外部定义,但是在外部定义时不能重复static关键字,static这里只能出现在类内部的声明语句。

  ④类的静态成员应该在类的外部初始化。

 

五.容器(线性容器及关联容器)

Ⅰ.线性容器

也称为顺序容器,凡是线性容器,都可以快速访问容器内元素。

需要关注的是不同容器在添加或删除元素时的代价,以及各个容器的特性。

>1.vector

  可变大小数组,支持快速随机访问,除开尾部之外的插入删除元素方式较慢。

>2.deque

  双端队列,支持快速随机访问,在头尾位置插入和删除速度较快。

  ①deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间中

  当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器

>3.array

  固定大小数组,支持快速随机访问,无法添加删除元素。

>4.list

  双向链表,只支持双向顺寻访问,任何位置进行插入操作都较快

>5.forward_list

  单项链表,只支持单向顺序访问,任何位置进行插入操作都较快

>6.string

  与vector相似,但专门用于保存字符,支持随机访问,尾部插入删除速度较快

>7.与线性容器的大小相关的构造关联容器并不支持大小参数

  

>8.各容器的成员函数使用

  插入,删除,访问等

Ⅱ.关联式容器

线性容器中的元素通过位置来访问;关联容器中的元素通过关键字来保存和访问

关联式容器支持高效的关键字查找和访问,最主要的容器是 map、set (头文件分别为map和set)。map和set

都不允许关键字重复。

可以用用有序和无需区分关联容器

按关键字有序保存元素:

>1.map,保存键值对<k-v>

  ①初始化时,分别指定键和值的类型,eg:map<string, int>  map1 = { {"aaa", 1}, {"bbb", 2}, {"ccc", 3}…… }(显式);

  ②map系列容器的元素类型是 pair:

>2.set,只保存关键字,关键字即为值

  ①指定键的类型即可,eg:set<int>  set1 = { 1,2,3…… };

>3.multimap,允许关键字重复的map

>4.multiset,允许关键字重复的set

无序的元素集合:

>5.unordered_map,利用哈希组织的map

>6.unordered_set,利用哈希组织的set

>7.unordered_multimap,允许关键字重复的unordered_map

>8.unordered_multiset,允许关键字重复的unordered_set

>9.各容器的成员函数使用

   插入,删除,访问等,下图为其中一部分:

 

六.拷贝控制

在定义任何C++类时,拷贝控制操作都是必要部分。一个类通过定义五种特殊的成员函数来控制这些操作:

拷贝构造函数、拷贝赋值运算符、析构函数

如果我们不显式的定义这些操作,编译器也会默认为我们定义,但编译器所定义的版本的行为可能并非我们所想要的。

Ⅰ.拷贝构造函数

将一个构造函数的第一个参数定为自身类类型的引用(非引用类型会导致传参时陷入无限循环调用的错误),并且任何额外参数都有缺省值,此函数成了拷贝构造函数。

class A
{
public:
    A();   //构造函数
    A(const A&); // 拷贝构造函数(const不是必须,但一般总是这样定义)
    ……
}

>1.和构造函数相似,没有显式定义的情况下,编译器会默认生成一个拷贝构造函数(称为合成拷贝构造函数)。一般情况下,合成拷贝构造函数会将参数成员逐个用对应方式拷贝到正在创建的对象中。

>2.拷贝初始化通过拷贝构造函数完成。但也有例外(类中定义移动构造函数的情况)。

>3.拷贝初始化发生的场景:

  ①使用 = 定义变量时

  ②将一个对象作为实参传递给一个非引用类型的形参

  ③使用花括号列表初始化一个数组中的元素或是一个聚合类中的元素。

  ④初始化标准库容器或是调用其insert或是push成员的时候。(相对的是,使用emplace创建的元素都是进行直接初始化)

>4.编译器有可能绕开拷贝构造函数

可能会出现以下情况:
源代码:string str1 = "NomoreFunc"; //拷贝构造 
实际执行的 :string str1("NomoreFunc"); //编译器略过了拷贝构造函数

但即便编译器进行了绕过,拷贝构造函数还是必须要存在且可访问的。

Ⅱ.拷贝赋值运算符

本质上是写了一个以类对象为参数的 = 运算符重载函数。

与构造函数相似,若是没有显式定义,编译器会默认生成合成拷贝赋值运算符,但还是那句话,不显式定义的话可能无法起到预想的作用。

Ⅲ.析构函数

析构函数执行的操作与构造函数相反,他会释放对象所使用的资源,并销毁对象的数据成员(非static类型的)。同样,不显式定义的话,编译器会默认生成合成析构函数。

>1.由于析构函数不接受参数,所以无法被重载。

>2.对于一个类,只能有唯一的一个析构函数。

>3.在一个析构函数中,首先执行函数体,然后销毁成员,销毁顺序为初始化顺序的逆序。

>4.析构函数中的析构是隐式的,成员销毁时发生什么纯粹取决于类型,比方式类类型的成员在析构时就回去调用它对应的析构函数。

>5.隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

 

七.动态内存

C++支持动态分配对象,其分配的对象的生命周期与创建位置无关,只有当显式的被释放时,这些对象才会销毁。

C++的内存管理是通过两个运算符完成的:new(在动态内存中为接受的对象分配空间并返回一个指向该空间的指针,并且可以选择对对象初始化)  delete(接受一个动态对象的指针,销毁并释放相关联的内存)

Ⅰ.智能指针

为了更安全的使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。(被管理的对象应该被释放时,指向它的智能会自动释放它)

两种之智能指针的区别在于管理底层指针的方式。智能指针使用方式与普通指针类似,解引用返回所指的对象,也可以进行判空。

>1.shared_ptr,允许多个指针指向同一个对象

  ①初始化 shared_ptr<T> sp 

  ②make_shared的使用(最安全的动态内存分配方法)

  make_shared<T>(arg)   返回一个shared_ptr,指向一个动态分配的类型为T的对象。并使用arg初始化此对象。

>2.weak_ptr (一种伴随类定义,指向shared_ptr所管理的对象,用来解决循环引用的问题)

>3.unique_ptr,独占所指向的对象

  初始化 unique_ptr<T> up

Ⅱ.动态数组

  new 和 delete 运算符一次分配或是释放一个对象,但是存在一次为很多对象分配内存的功能。

通过new 可以分配和初始化一个对象数组。同时delete可以销毁一个对象数组。

 

.重载运算

重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和后跟的要定义的运算符号组成。

重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。但如果作为成员函数,则他的第一个(左侧)运算对象绑定到隐式的this指针上,所以函数数量会比运算符的运算对象总数少一个。

Ⅰ.某些运算符不该被重载

 

 ps. ①由于C++语言已经定义了逗号(,)和取地址符(&)用于类类型对象时的特殊含义。所以一般这两个符号也不进行重载。

      ②赋值(=),下标([]),调用( ( ) )和成员访问箭头(->)运算符必须是成员函数重载

Ⅱ .关系运算符的定义

  定义了相等运算符(==)的类一般也会定义关系运算符。通常是关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用。

Ⅲ.下标运算符

  表示容器的类通常可以通过位置访问元素,所以一般会定义下表运算符重载 operator[]。

Ⅳ.成员访问运算符

  ①箭头运算符必须是类的成员,解引用运算符通常也是(不一定)。

  ②重载的箭头运算符必须返回类的指针或是自定义了的某个类的对象。

 

九.面向对象的程序设计(OOP:object-oriented-programming)

面向对象程序设计基于三个基本概念:数据抽象、继承、动态绑定。这里主要说明继承和动态绑定方面,他们对程序编写有两方面的影响:

一是使我们可以更容易地定义与其它类相似但不完全一致的新类;二是在使用这些彼此相似的类编写代码时,又可以一定程度上忽略掉他们的区别。

Ⅰ.OOP概述

>1.继承

  ①通过继承联系在一起的类构成一种层次关系。层次关系的根部是基类,其他类则是直接或是间接的从基类继承而来,通过继承得到的类称为派生类

  ②基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

  ③C++中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。有些函数,基类希望他的派生类可以各自定义适合自身的版本,此时基类就会将这些函数声明为虚函数。通过在成员函数声明前面加 virtual 来定义虚函数,使得该函数执行动态绑定。

>2.动态绑定

  通过使用动态绑定,我们可以用同一段代码分别处理不同类的对象。当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定(如果是通过一个具有普通类型的表达式调用虚函数时,在编译时就会将调用的版本确定下来)。又由于发生过程中函数的运行版本由实参决定(即在运行时选择函数的版本),所以动态绑定有时又被称为运行时绑定。

 Ⅱ.访问控制与继承

>1.访问控制

  ①派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。

  ②派生类能访问公有成员,而不能访问私有成员。

  ③对于某些成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。此时使用 protected 访问运算符说明这种成员。

>2.派生类构造函数

  派生类必须使用积累的构造函数来初始化他的基类部分。每个类只控制它自己的成员初始化过程。

  执行构造函数时,先调用基类的构造函数初始化基类部分,然后按照声明顺序依次初始化派生类的成员。

>5.派生类使用基类的成员

  派生类可以访问基类的公有成员和受保护成员。

>6.继承与静态成员

  ①如果基类定一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。

  ②静态成员遵循通用的访问规则控制。

>7.可以通过使用 final 来防止继承的发生。

>8.派生类对象及派生类向基类的类型转换

  ①在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。

  ②因为在派生类对象中还有与积累部分对应的组成部分,所以可以把派生类的对象当成基类对象来使用,而且也能将基类的指针或引用绑定到派生类对象中的基类部分上。

 ps.编译器隐式的执行派生类到基类的转换。

>9.存在继承关系的类之间的转换规则

  ①从派生类向基类的类型转换只对指针或引用有效。

  ②基类向派生类不存在隐式类型转换。

  ③派生类向基类的类型转换也可能会由于访问受限而不可行。

Ⅲ.虚函数  

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确认应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

也可以手动回避虚函数的动态绑定机制:

Ⅳ.抽象基类

  含有纯虚函数的类是抽象基类

  纯虚函数:通过在声明语句的分号之前添加 “ = 0 ” 来说明一个虚函数为纯虚函数。 

Ⅴ.虚析构函数的应用掌握

 

差不多这些吧,总觉得还是有点多了,部分过于基础的部分其实主要是为了复习一遍,但大致基础知识就到这里了,没有写到的C++模板以及容器的详解就留到下次的文章了~

posted @ 2022-02-23 10:47  Kaniso_Vok  阅读(79)  评论(0编辑  收藏  举报