技术/语言/C++入门课

技术/语言/C++入门课

C/C++ 课程笔记

绪论

面向对象

  • 面向对象的核心: 封装、继承、多态、
  • 面向对象程序设计的四个阶段
    • 系统分析、系统设计、对象设计和对象实现

编译技术

  • 编译过程:

    • 预处理
    • 词法分析:产生一个程序的单词序列
    • 语法分析:检查程序语法结构
    • 代码生成:生成中间代码
    • 模块连接:中间代码与库连接,形成一个可执行的程序
  • 静态链接 & 动态链接:代码与函数库的连接方式

  • 静态绑定 & 动态绑定:计算函数入口地址的方式

C++基础

重点:const、引用类型

强类型

  • 为所有变量指定数据类型
  • 先声明后使用
  • 不同类型的变量相互操作时应加类型转换

常量

const、volitile和mutable

volitile 类型:易失性,优化器不对该变量的读取进行优化,用到该变量时重 新从它所在的内存读取数据。 修饰的变量可由操作系统、硬件、并发执行的线程在程序中进行修改。

mutable类型: 可变性,是const 的反义词,为突破 const的限制而设置的;被mutable 修饰的变量永远处于可变得状态,即使在 const函数中; mutable只能用来修饰数据成员;不能与 const、volatile 或 static 同时出现

  • 定义一个常量,必须在定义时,赋初值;
  • const 修饰符与类型修饰符可以对调 ;
  • 常量不能在赋值号左边出现;
  • 常量可以在赋值号右边出现;
  • 常量的地址,只能赋值给一个常量指针;

强制访问

  • 常量的访问:常量的地址,可以进行强制地址类型转换; 转换后,可以在赋值号左右都出现;绕过const的方法:对其地址进行强制类型转换,再解析。
  • 外部对私有成员访问:对对象取地址然后进行指针的强制类型转换

类与常量

  • 常成员函数
    • returnType func(...) const;
    • 不可修改非静态数据成员
    • 不得调用其他非 const 的成员函数
  • 常对象
    • 类名 const 对象名(实参表);const 类名 对象名(实参表);
    • 整个对象的数据成员都不允许修改。 在定义常对象时一定要初始化。

引用

  • 定义引用变量时就一定要正确初始化,初始化后指向不可变
  • 引用变量中存放的是被引用变量的地址; 其本质是指针;
  • 在有串接(多级)引用时,都是指向最终被引用的变量; 与二级(多级)指针有很大的差别;
  • 使用引用变量,操作对象都是被引用的变量
  • 引用必须与合法的存储单元关联,不存在无法取址的量的引用(比如函数返回的临时值)

函数参数及返回值

  • 如果某个参数给了缺省值,其右边的参数都需要给参数值。

左右值表达式

右值引用

  • 格式:type &&x
  • 右值引用也必须立即进行初始化操作,且只能使用右值进行初始化
  • 右值引用主要用于移动语义(因此可以修改右值)和完美转发
  • 传递右值的引用的两种方式:c++11的右值引用type &&x、常量左值引用const type &x
  • 函数返回右值引用时,返回值不可修改

其他

函数调用时按参数列表从右至左顺序压入栈区

类基础

关键字:深拷贝、浅拷贝

缺省构造

若程序员没有主动给类定义构造函数,编译器自动给一个缺省的构造函数。一旦程序员定义了一个构造函数,系统将不会再给出缺省构造函数。除了系统缺省的构造函数外,只要构造函数无参或者参数有缺省值, 编译器会认为它就是缺省构造函数。缺省的构造函数同时只能有1个。

析构函数一般用来释放体外空间,不用来释放本结点空间。

拷贝构造

当实例中有指针指向堆区内存时,浅拷贝得到的指针指向同一堆区空间,析构的时候就会相互影响

默认浅拷贝构造函数:Student(stu)

非法的复制构造函数:Student(Student a)

自定义深拷贝构造函数:Student(const Student &a)

移动构造

  • 函数签名:A::A(A&& q)
  • 右值引用,遇到右值则优先于深拷贝构造。
  • 功能:完成体外空间的交接以及原体外空间指针的置空(防止析构出错)。

堆分配及其初始化

  • new <类型表达式> 类型表达式是什么类型,返回的就是什么类型的指针
  • delete <指针>;
    • 指针指向非数组的单个实体
    • 如sq指向对象,则自动调用析构函数,再释放对象所占的内存。
  • delete [ ] <数组指针>;
    • 指针指向任意维的数组时使用
    • 对所有对象(元素) 自动调用析构函数。
    • 若数组元素为简单类型,则可用delete <数组指针>代替
    • delete 并不会销毁指针本身,安全起见要手动赋值NULL
  • 注意 new[]/delete[], new/delete, malloc/free 要搭配使用

栈分配及其初始化

  • 形式:

    • 对于单个实例:type obj(...)

    • 对于对象数组:type arr[5] = {{arr[0]的构造参数}, ...} 或者去掉等号(这种方式简单类型的变量初始化也可使用:int a{1}type arr[5] {{arr[0]的构造参数}, ...}

数据成员的初始化

  • 在定义数据成员时赋初值,等价于在构造函数体前赋初值;
  • 须在构造函数体前初始化: 只读成员、引用成员、对象成员
  • 在定义对象时,自动调用构造函数初始化;
  • 按定义的先后次序初始化,与出现在初始化位置列表的次序无关;
  • 普通数据成员没有出现在初始化位置时,若所属对象为全局、静态 或new的对象,将具有缺省值0
  • 基类和非静态对象成员没有出现在初始化位置时,此时必然调用无参构造函数初始化
  • 如果类仅包含公有成员且没有定义构造函数,则可以采用同C兼容的初始化方式,即可使用花括号初始化数据成员;
  • 联合类型的对象只须初始化一个成员(共享内存);

构造函数体前初始化:只读成员、引用成员、对象成员

  • 位置:函数签名之后,{ } 之前,:分隔

  • 各数据成员以,分隔

  • 用构造函数的形式给各变量赋初值,如 y(t)

  • 可以用列表的形式{} 赋初值,如 y{t}

  • 不能采用 = 来初始化 : y=t // error

成员指针

  • offsetof(类名, 成员名) 返回成员偏移的字节数(返回int类型)
  • 成员指针的获取(在类中取址)return &<类名>::<成员名>
  • 成员指针的申明:只需要将申明一般指针用的 type * 改为 type <类名>::* 即可
  • 成员指针的解析:<实例>.*<成员指针><对象指针>->*<成员指针>
  • 普通成员指针是偏移量,不可移动,不可强制类型转换
  • 成员指针为空(即= 0时)时,会自动指向0xffffffff的内存单元
  • 构造函数不可取成员指针(否则可能造成手动调用构造函数)
  • 静态成员指针与普通指针除了访问权限外没有差别

静态成员

  • 静态数据成员用 static 声明

  • 静态函数成员的访问权限及继承规则同普通函数成员没有区别

  • 不可在体前初始化

  • 静态数据成员独立分配内存,**不属于任何对象内存 **

  • 用于描述类的总体信息,如对象总数、连接所有对象的链表表头等。

  • 静态函数成员没有this指针

  • 访问方式:

    • 类名::静态成员(…)
    • 对象.静态成员(…)
    • 对象.类名::静态成员(…)
  • 构造函数、析构函数以及虚函数、常函数等必须有 this 参数,不能声明为static

  • sizeof(<obj>)不计算<obj>的静态数据成员的大小

对象的存储机制

继承与权限机制

友元

  • 友元函数 class A {... friend func(xxx); ... }
    • 不受访问权限的限制
    • 可以在任何访问权限下用friend 可以访问当前类的任何成员
    • 一个函数可成为多个类的友员
  • 友元类 class A {... friend class B; ...}
    • 类B 的所有函数成员都是类A的友元
    • 友元关系不传递、不对称

单继承

单继承范式
1
2
3
4
5
class <派生类名>: [<派生控制符/继承方式>] <基类名> {
<派生类新定义成员>
<派生类重定义基类同名的数据和函数成员>
<派生类声明恢复基类成员访问权限>
}
  • 派生控制权限只降不升
  • 恢复控制权限可升可降,私有不可访
恢复权限实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A 
{
protected: int x;
private: int y;
public:
void setx(int m) { ... }
};
//派生类声明恢复基类成员访问权限
Class B : private A
{
public:
A::setx; // getx后面没有括号
protected:
A::x; // 数据成员
private:
A::y; // 不可访问A类的私有成员
}

  • C++允许父类指针指向子类对象、父类引用子类对象, 无须通过强制类型转换, 编译时按父类说明的权限访问成员

虚函数

C++ 的多态特性与虚函数密不可分,所以首先要了解多态性是什么:

  • 具有相似功能的不同函数使用同一名称

  • 用相同的调用方式,调用不同功能的同名函数

  • 静态多态

    • 函数名相同,参数不同
  • 动态多态

    • 函数名相同、参数相同
    • 分属不同的类:基类与多个派生类
  • 多态的实现:

    • 联编:绑定、装配,将一个标识符名和一个地址联系在一起。
    • 静态/早期联编:实现编译时多态——函数重载、运算符重载
    • 动态/后期联编:实现运行时多态——虚函数
  • 纯虚函数virtual returnType funName(...) = 0 (类比于java的抽象函数),包含纯虚函数的类为抽象类,不论纯虚函数在体外是否有定义。
  • 通过指针调用虚方法时才会查虚函数表(晚期绑定)。数据成员才会随着对象的构造滑入内存,方法不会。
  • 析构函数需要成为虚函数以保证析构的正确性
  • 有虚函数的类对象内存初始单元指向一个虚函数表,虚函数表内包含派生类的虚函数和派生类没有覆盖到的父类的虚函数

虚函数表的绑定

申明
1
2
3
4
5
6
7
8
9
10
class A {
public:
A();
virtual ~A();
}; //对应虚函数表VFTA
class B: A {
public:
B();
virtual ~B();
}; //对应虚函数表VFTB

当构造一个B类对象obj,然后析构obj,在这段时间内发生的活动:

  1. 对象初始地址指向VFTA
  2. 调用A的构造函数(此时A类对象调用的虚函数与VFTA绑定,执行的虚函数将是A类的函数)
  3. 对象初始地址指向VFTB
  4. 调用B的构造函数(此时B类对象调用的虚函数与VFTA绑定,执行的虚函数将是B类的函数)
  5. 调用析构函数(此前对象的初始单元指向VFTB)
  6. 对象初始地址指向VFTA
  7. 调用析构函数

多继承

格式
1
2
3
4
5
6
Class B: [virtual] [派生控制] 基类1, [派生控制][virtual] 基类2
{ ... };

派生控制:private, protected , public
缺省派生控制:private
在基类名称前若有:virtual , 虚基类

虚基类

  • 虚基类共享同一片存储空间,解决了同一个物理对象初始化两次的问题(当一个物理对象只需要一个时使用)
  • 虚基类与基类同名时,二者拥有独立的存储空间。
  • 相同的基类不能被同时直接继承,但可以同时间接继承

构造与析构顺序

  • 画出继承关系树(按定义顺序从左至右排列基类,箭头由子类指向父类),虚基类用方框框起来;
  • 实例
申明
1
2
3
4
5
6
7
8
9
10
11
#include <iostream.h>
struct A{ A(){ cout<<'A';} };
struct B { const A a; B( ) { cout<<'B';} }; //对象成员a将作为新 根
struct C{ C(){ cout<<'C';} };
struct D{ D(){ cout<<'D';} };
struct EA{ E(){ cout<<'E';} };
struct FB, virtual C{ F(){ cout<<'F';} };
struct GB{ G(){ cout<<'G';} };
struct Hvirtual C, virtual D{ H(){ cout<<'H';} };
struct IE, F, virtual G, H{ E e; F f; I(){ cout<<'I';} };
void main(void) { I i; }
多继承构造树实例

运算符重载

可重载类型

各种函数可重载的运算符

强转重载

  • 单参数的构造函数具备类型转换作用,必要时能自动将参数类型的值转换为要构造的类型。

  • 用operator定义强制类型转换函数: operator 类型(…)

    • 强制类型转换函数不需要定义返回类。
  • 当用explicit修饰强转重载或单参数构造函数时,相关的隐式类型转换将不会自动进行,必须显式调用

递增(减)重载

  • 前置++/--重载为单目运算符
  • 后置++/--重载为双目运算符(多出来的那一目必须为int类型参数)
  • 注意普通成员函数内隐含了一个this参数(占一目)

括号重载

  • 构造函数的括号不受括号重载的影响

模板

模板机制

函数模板

1
2
3
4
template  <模板形参表> 
返回类型 函数名 (参数表) {
<函数体>
}
  • <模板形参表>可以包含一个或多个用逗号分开的参数,每一项均有内置关键字classtypename引导一个标识符 T
  • 此标识符为模板参数,表示一种数据类型
  • type是基本数据类型,class是类类型
  • 参数表中至少有一个参数说明
  • 参数在函数体中至少应用一次
  • 类模板同上

类模板

1
2
3
4
5
6
7
template   <模板形参表>  
class 类名 {
<类体说明>
};

//类模板的成员函数体外定义时
类名<T>::成员函数(params) {...}

模板与继承

  • 类模板可以从非模板类派生(注意模板类和类模板的区别)
  • 类模板可以从类模板派生
实例
1
2
template <class T>
class A: public B<T> {...}; //B是模板形参表有一个class类型参数的类模板
  • 非模板类可以从类模板派生

注意事项

  • 类模板的成员函数是在调用时才被创建,导致分文件编写时调用不到
    • 解决方案:将实现与申明写在同一个文件,推荐使用.hpp

异常处理

异常

  • 抛出的异常分类:内置数据类型异常,标准异常,自定义类对象,不接任何对象(throw;只能用于于catch块中,作用是将异常传递到上一级catch块)
  • try-throw-catch:监视-抛出-捕获,catch到一个对象时调用以对象为参数的构造函数(拷贝/移动构造)
  • 异常接口:returnType func() throw(...) {}noexceptthrow()几乎等价)
  • set_terminate(), set_unexpect():设置自定义终止函数(异常传递到顶层时调用终止函数),设置自定义处理不可预料异常的函数(函数产生有悖于异常接口说明的异常类型时调用)。

析构问题

  • 如果通过new产生的指针类型的异常,在catch 处理后,通常应使用delete释放内存;
  • 如果继续传播指针类型的异常,则可以不使用 delete;从最内层被调函数抛出异常到外层调用函数的catch处理过程捕获异常,由此形成的函数调用链所有局部对象都会被自动析构,因此使用异 常处理机制能在一定程度上防止内存泄漏;
  • 调用链中通过new分配的内存不会自动释放
  • 特殊情况在产生异常对象的过程中也会出现内存泄漏情况:未完成构造的对象。

异常与继承

  • 如果父类A的子类为B,B类异常能被catch(A)、 catch(const A)、 catch(volatile A)、 catch(const volatile A)等捕获。

  • 如果父类A的子类为B,指向可写B类对象的指针异常也能被catch(A*)、 catch(const A*)、 catch(volatile A*)、 catch(const volatile A)等捕获。

  • 因此捕获子类对象的catch应放在捕获父类对象的 catch前面。

  • catch(const volatile void *)能捕获任意指 针类型的异常,catch(…)能捕获任意类型的异常。

断言

  • <asset.h>
  • asset(int expr),expr为假时进入断言函数(会输出断言表达式、断言发生的文件名和行号)
  • 静态断言static_asset(int expr)(编译时检查)

类型推导与转换

传统转换

显式转换

  • 格式:(T)expr

  • 一般类型转换得到右值: (T)expr,**左值引用转换得到左值: **(T&)expr

  • const T无法转为T,但可以通过指针类型*(T*)&或引用类型(T&)来转换

隐式转换

  • 由编译器自动完成
  • 场景:传参时、赋值时、运算时
  • 转换规则:
    • 非浮点类型字节少的向字节数多的转换:bool、char、short和int的运算按int类型进行。
    • 非浮点类型有符号数向无符号数转换:所有浮点常量及浮点数的运算按double类型进行。
    • 运算时整数向double类型的转换。

cast转换

  • 格式:XXX_cast<T>(expr) //将expr转化为目标类型T
  • static_cast
    • 编译时检查是否可以转换
    • 不能去除源类型的constvolatile
    • 目标类型不能包含存储位置类修饰符,如static、extern、 auto、register
  • const_cast
    • 目标类型必须是指针、引用、或者指向对象成员的指针
    • 类型表达式不能包含存储位置类修饰符
    • 不能将无址常量、位段访问、无址返回值转换为有址引用
  • dynamic_cast
    • 目标类型必须是类的引用、类的指针或者void*类型
    • 源类型必须是类的对象、对象地址
    • 用于子类向父类转换,以及有虚函数的基类向派生类转换
    • 不能去除源类型中的const和volitale
  • reinterpret_cast
    • 用于有址引用与无址引用之间的相互转换、指针或引用类型的转换、足够大整数与指针类型的转换

typeid

  • 获得对象的真实类型标识
  • 格式
    • typeid(类型表达式)
    • typeid(数值表达式)
  • typeid的返回结果是const type_info&类型
  • 在使用typeid之前可先#include <typeinfo>,使用std名字空间。

decltype

  • 提取表达式的类型,构成新的类型表达式
  • decltype(expr)
decltype 实例
1
2
3
4
5
6
int x = 10;
decltype(x) y; // int y;

int a1[10];
decltype(a1) p1; // int p1[10]; p1的类型是 int[10]
decltype(a1) *p11; // int (*p11)[10]

Lamda 表达式

  • 形式:[捕获列表](形参列表) mutable 异常说明->返回类型{函数体}
  • 使用:auto fuc = [Lambda expr]; fuc(params);
  • mutable: 可选,用于说明是否可以修改捕获变量
  • 捕获列表说明:
    • 捕获列表的参数用于捕获Lambda表达式的外部变量;
    • 外部变量可以是函数参数或函数定义的局部自动变量;
    • 出现&变量名表示引用外部变量;
    • [&]表示捕获所有函数参数或函数定义的局部自动变量。
    • 出现=变量名表示使用外部变量的值(值参传递),
    • [=]表示捕获所有函数参数或函数定义的局部自动变量的值;
    • 外部变量不能是全局变量或static定义的变量;
    • 外部变量不能是类的成员;
    • 参数表后有mutable表示在Lambda表达式可以修改“值参传递的值”,但不影响Lambda表达式外部变量的值。

Lambda表达式被编译为临时类的对象,临时类重载了函数operator()