跳至主要內容
CPP

CPP

丈墨...大约 37 分钟c/cppCPP

CPP

在菜鸟教程上学习 CPPopen in new window 时的一些笔记。

注意

由于本人也正在上有关 CPP 的课程,对 CPP 的知识也是了解一些,所以也只会记录一些重要的,或不知道的知识。

基础教程

简介

C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。
C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。
C++ 是由 Bjarne Stroustrup 于 1979 年在新泽西州美利山贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,最初命名为带类的 C,后来在 1983 年更名为 C++。
C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。

面向对象的程序设计有四大特性:

  • 封装(Encapsulation):封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。
  • 继承(Inheritance):继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。
  • 多态(Polymorphism):多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。
  • 抽象(Abstraction):抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。

数据类型

宽字符型:wchar_t,原型为typedef short int wchar_t;,所以 wchart_t 实际的空间占用与 short int 相同。

类型转换:

  • 静态转换(Static Cast):静态转换是将一种数据类型的值强制转换为另一种数据类型的值。静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。
    int i = 10;
    float f = static_cast<float>(i); // 静态将int类型转换为float类型
    
  • 动态转换(Dynamic Cast):动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。
    class Base {};
    class Derived : public Base {};
    Base* ptr_base = new Derived;
    Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针
    
  • 常量转换(Const Cast):常量转换用于将 const 类型的对象转换为非 const 类型的对象。常量转换只能用于转换掉 const 属性,不能改变对象的类型。
    const int i = 10;
    int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
    
  • 重新解释转换(Reinterpret Cast):重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。
    int i = 10;
    float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
    

变量类型

变量定义:变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。
变量声明:变量声明向编译器保证变量以给定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。可以使用 extern 关键字在任何地方声明一个变量。虽然可以在 CPP 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。
变量初始化:赋予有意义的值。

CPP 中的左值和右值:

  • 左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
  • 右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。(将亡对象)

变量作用域

类作用域:类内部声明的变量。

示例代码
#include <iostream>

class MyClass {
public:
    static int class_var;  // 类作用域变量
};

int MyClass::class_var = 30;

int main() {
    std::cout << "类变量: " << MyClass::class_var << std::endl;
    return 0;
}

以上实例中,MyClass 类中声明了一个名为 class_var 的类作用域变量。可以使用类名和作用域解析运算符 :: 来访问这个变量。在 main() 函数中访问 class_var 时输出的是 30。

常量

与 C 语言不同,CPP 有布尔常量,true 值代表真,false 代表假。不应该把 true 值看成 1,false 值看成 0。

字符常量是括在单引号中。如果常量以 L(仅当大写时)开头,则表示它是一个宽字符常量(例如 L'x'),此时它必须存储在 wchar_t 类型的变量中。否则,它就是一个窄字符常量(例如 'x'),此时它可以存储在 char 类型的简单变量中。

类型限定符:

  • const const 定义常量,表示该变量的值不能被修改。。
  • volatile 修饰符 volatile 告诉该变量的值可能会被程序以外的因素改变,如硬件或其他线程。。
  • restrict 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。
  • mutable 表示类中的成员变量可以在 const 成员函数中被修改。
  • static 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问。
  • register 用于定义寄存器变量,表示该变量被频繁使用,可以存储在 CPU 的寄存器中,以提高程序的运行效率。

存储类

  • auto:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。
  • static:指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。在 C++ 中,当 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
  • extern:用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用 'extern' 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。当有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。
  • mutable:允许对象的成员替代常量。也就是说,mutable 成员可以通过 const 成员函数修改。
  • thread_local:使用 thread_local 说明符声明的变量仅可在其创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。thread_local 不能用于函数声明或定义。

小知识

vector 是表示可以改变大小的数组的序列容器。
与 arrays 一样,vector 对元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问它们的元素,并且与在数组中一样高效。但是与 arrays 不同,它们的大小可以动态变化,容器会自动处理它们的存储。

函数

Lambda 函数与表达式:Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
形式:[capture](parameters) mutable ->return-type{body},其中,若没有返回值:[capture](parameters){body}
在 Lambda 表达式内可以访问当前作用域的变量,这是 Lambda 表达式的闭包(Closure)行为。 与 JavaScript 闭包不同,C++变量传递有传值和传引用的区别。可以通过前面的[]来指定:

[]      // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&]     // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=]     // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x]  // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。

另外有一点需要注意。对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:[this]() { this->someFunc(); }();

数字

数学运算,引入头文件 <cmath> ,CPP 中也可以引入 C 语言中的数学头文件 <math>
随机数,引入头文件 <cstdlib> ,使用 rand() 函数生成一个伪随机数。生成随机数之前必须先调用 srand() 函数设置种子。

字符串

支持 C 语言的字符串使用风格(数组),也引入了 string 类类型,除了能够完成 C 语言字符串的处理功能,还增加了其他功能。

引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

与指针的区别:不存在空引用;创建时必须初始化;初始化为一个对象后,就不能指向另一个对象。

声明:

int&  r = i;
double& s = d;
//其中,r,s是引用,i,d是已声明变量

引用可以用于函数的参数列表和函数返回值。

基本的输入输出

I/O 库头文件:

  • <iostream> 该文件定义了 cin、cout、cerr 和 clog 对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。
  • <iomanip> 该文件通过所谓的参数化的流操纵器(比如 setw 和 setprecision),来声明对执行标准化 I/O 有用的服务。
  • fstream> 该文件为用户控制的文件处理声明服务。我们将在文件和流的相关章节讨论它的细节。

1、标准输出流(cout):预定义的对象 cout 是 iostream 类的一个实例。cout 对象"连接"到标准输出设备,通常是显示屏。cout 是与流插入运算符 << 结合使用。流插入运算符 << 在一个语句中可以多次使用,endl 用于在行末添加一个换行符。
2、标准输入流(cin):预定义的对象 cin 是 iostream 类的一个实例。cin 对象附属到标准输入设备,通常是键盘。cin 是与流提取运算符 >> 结合使用。流提取运算符 >> 在一个语句中可以多次使用,用于要求输入多个数据。
3、标准错误流(cerr):预定义的对象 cerr 是 iostream 类的一个实例。cerr 对象附属到标准输出设备,通常也是显示屏,但是 cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。cerr 也是与流插入运算符 << 结合使用。
4、标准日志流(clog)预定义的对象 clog 是 iostream 类的一个实例。clog 对象附属到标准输出设备,通常也是显示屏,但是 clog 对象是缓冲的。这意味着每个流插入到 clog 都会先存储在缓冲区,直到缓冲填满或者缓冲区刷新时才会输出。clog 也是与流插入运算符 << 结合使用。

面向对象

类 & 对象

1、类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。

2、访问数据成员:类的对象的公共数据成员可以使用直接成员访问运算符 “.” 来访问。

3、访问修饰符:

  • 公有(public)成员:公有成员在程序中类的外部是可访问的。
  • 私有(private)成员:私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。实际操作中,一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。
  • protected(受保护)成员:protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

4、构造函数:类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。

5、初始化列表来初始化字段:假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:

C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
  ....
}

6、析构函数:类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

7、拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数,在函数内部被使用。
  • 复制对象,并从函数返回这个对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个,但只会进行浅拷贝。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数,避免多个对象共享同一指针或出现资源泄漏等问题。拷贝构造函数的最常见形式如下:

classname (const classname &obj) {
   // 构造函数的主体
}
// 在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。

8、友元:类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。定义时使用 friend 关键字修饰。友元函数没有 this 指针。

9、内联函数:通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

说明

引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的 i 节省。所以内联函数一般都是 1-5 行的小函数,不超过 10 行。在使用内联函数时要注意:

  • 在内联函数内不允许使用循环语句和开关语句;
  • 内联函数的定义必须出现在内联函数第一次调用之前;
  • 类结构中所在的内部定义的函数是内联函数。

10、this 指针:在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。
this 是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。

11、指向类的指针:一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。

12、类的静态成员:使用 static 关键字来把类成员定义为静态的。当声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。

静态成员函数:如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
但是,静态成员函数与普通成员函数是有区别的:

  • 静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
  • 静态成员函数有一个类范围,他们不能访问类的 this 指针。

继承

1、允许依据已有的类来定义一个新的类。已有的类称为基类,新建的类称为派生类。
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,使用一个类派生列表来指定基类,派生列表以一个或多个基类命名。

class derived-class: access-specifier base-class
// 访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,默认为 private。base-class 是之前定义过的某个类的名称。

2、派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

3、一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

4、继承中的特点:

  • public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private;
  • protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private;
  • private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

5、多继承:多继承即一个子类可以有多个父类,它继承了多个父类的特性。

语法

class <派生类名>:<继承方式 1><基类名 1>,<继承方式 2><基类名 2>,…
{
<派生类类体>
};

函数重载和运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。

函数重载:在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。

运算符重载:可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
(老白说的要考虑的因素:语义、参数、返回值、是否级联)

小知识

重载前缀自增自减运算符时不带参数,重载后缀自增自减运算符时设置一个无用参数 int 来进行区别。

多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

虚函数:是在基类中使用关键字 virtual 声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

纯虚函数:有时可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

// 在基类中定义如下
// pure virtual function
virtual int fun() = 0;
// = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。

数据抽象

数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。
通过访问标签进行强制抽象,即使用 public、protect、private 访问标识符。
数据抽象的优势:

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
  • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

数据封装

封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。(这里区别说起来不同,但是感觉做的事情跟数据抽象很像)

接口(抽象类)

接口描述了类的行为和功能,而不需要完成类的特定实现。C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
设计抽象类(通常称为 ABC)的目的,是给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。可用于实例化对象的类被称为具体类。子类必须实现每个纯虚函数才能实例化子类对象。

知识总结

定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

高级教程

文件和流

从文件读取流和向文件写入流需要用到 C++ 中另一个标准库 <fstream>
定义的三个数据类型:

  • ofstream :该数据类型表示输出文件流,用于创建文件并向文件写入信息。
  • ifstream:该数据类型表示输入文件流,用于从文件读取信息。
  • fstream:该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

打开文件:在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstream 和 fstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。
下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

void open(const char *filename, ios::openmode mode);
// 第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式,具体模式不在此列出
// 可以指定多个模式,eg:
ifstream  afile;
afile.open("file.dat", ios::out | ios::in );

关闭文件:当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。
下面是 close() 函数的标准语法,close() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

void close();
// eg:
aflie.close();

写入文件
在 C++ 编程中,我们使用流插入运算符( << )向文件写入信息,就像使用该运算符输出信息到屏幕上一样。唯一不同的是,在这里您使用的是 ofstream 或 fstream 对象,而不是 cout 对象。

读取文件
在 C++ 编程中,我们使用流提取运算符( >> )从文件读取信息,就像使用该运算符从键盘输入信息一样。唯一不同的是,在这里您使用的是 ifstream 或 fstream 对象,而不是 cin 对象。

文件位置指针:istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put")。seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。

异常处理

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块(捕获不同类型的异常)。

抛出异常:使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

除零异常实例
double division(int a, int b)
{
   if( b == 0 )
   {
      throw "Division by zero condition!";
   }
   return (a/b);
}

捕获异常:catch 块跟在 try 块后面,用于捕获异常。可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。

try
{
   // 保护代码
}catch( ExceptionName e )
{
  // 处理 ExceptionName 异常的代码
}
// 上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...,如下所示:
try
{
   // 保护代码
}catch(...)
{
  // 能处理任何异常的代码
}

标准的异常:C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。详细的异常描述见此open in new window

定义新的异常:可以通过继承和重载 exception 类来定义新的异常。

示例代码
#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};

int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}
// what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

动态内存

C++ 程序中的内存分为两个部分:

  • 栈:在函数内部声明的所有变量都将占用栈内存。
  • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。

在 C++ 中,可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。如果不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

new 运算符来为任意的数据类型动态分配内存的通用语法:new data-type;
在这里,data-type 可以是包括数组在内的任意内置的数据类型,也可以是包括类或结构在内的用户自定义的任何数据类型。(不在建议使用 malloc() 函数,new 不只是分配了内存,它还创建了对象)
在任何时候,当个已经动态分配内存的变量不再需要使用时,可以使用 delete 操作符释放它所占用的内存: delete pvalue;// 释放 pvalue 所指向的内存

数组的动态内存分配:

一维数组
// 动态分配,数组长度为 m
int *array=new int [m];

//释放内存
delete [] array;
二维数组
int **array;
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int [n];
}
//释放
for( int i=0; i<m; i++ )
{
    delete [] array[i];
}
delete [] array;
三维数组
int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int *[n];
    for( int j=0; j<n; j++ )
    {
        array[i][j] = new int [h];
    }
}
//释放
for( int i=0; i<m; i++ )
{
    for( int j=0; j<n; j++ )
    {
        delete[] array[i][j];
    }
    delete[] array[i];
}
delete[] array;

命名空间

作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

定义命名空间:命名空间的定义使用关键字 namespace,后跟命名空间的名称,如下所示:namespace namespace_name { // 代码声明 }
为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称,如下所示:name::code; // code 可以是变量或函数

using 指令:可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分,您可以使用如下的语句:using std::cout;。随后的代码中,在使用 cout 时就可以不用加上命名空间名称作为前缀,但是 std 命名空间中的其他项目仍然需要加上命名空间名称作为前缀。

不连续的命名空间:命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。

嵌套的命名空间:命名空间可以嵌套,可以在一个命名空间中定义另一个命名空间,如下所示:

namespace namespace_name1 {
   // 代码声明
   namespace namespace_name2 {
      // 代码声明
   }
}
// 可以通过使用 :: 运算符来访问嵌套的命名空间中的成员:
// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;

// 访问 namespace_name1 中的成员
using namespace namespace_name1;
// 在上面的语句中,如果使用的是 namespace_name1,那么在该范围内 namespace_name2 中的元素也是可用的

小知识

全局变量 variable 表达为 ::variable,用于当有同名的局部变量时来区别两者。

模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int>vector <string>
可以使用模板来定义函数和类。

函数模板:定义形式如下:

template <typename type> ret-type func-name(parameter list)
{
   // 函数的主体
}
// type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
示例代码
#include <iostream>
#include <string>

using namespace std;

template <typename T>
inline T const& Max (T const& a, T const& b)
{
    return a < b ? b:a;
}
int main ()
{

    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl;

    double f1 = 13.5;
    double f2 = 20.7;
    cout << "Max(f1, f2): " << Max(f1, f2) << endl;

    string s1 = "Hello";
    string s2 = "World";
    cout << "Max(s1, s2): " << Max(s1, s2) << endl;

    return 0;
}

类模板:声明形式如下:

template <class type> class class-name {
...
// type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。
}
示例代码
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
  private:
    vector<T> elems;     // 元素

  public:
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty();
    }
};

template <class T>
void Stack<T>::push (T const& elem)
{
    // 追加传入元素的副本
    elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::pop(): empty stack");
    }
    // 删除最后一个元素
    elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::top(): empty stack");
    }
    // 返回最后一个元素的副本
    return elems.back();
}

int main()
{
    try {
        Stack<int>         intStack;  // int 类型的栈
        Stack<string> stringStack;    // string 类型的栈

        // 操作 int 类型的栈
        intStack.push(7);
        cout << intStack.top() <<endl;

        // 操作 string 类型的栈
        stringStack.push("hello");
        cout << stringStack.top() << std::endl;
        stringStack.pop();
        stringStack.pop();
    }
    catch (exception const& ex) {
        cerr << "Exception: " << ex.what() <<endl;
        return -1;
    }
}

信号处理

信号是由操作系统传给进程的中断,会提早终止一个程序。
有些信号不能被程序捕获,但是下面所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。

  • SIGABRT 程序的异常终止,如调用 abort。
  • SIGFPE 错误的算术运算,比如除以零或导致溢出的操作。
  • SIGILL 检测非法指令。
  • SIGINT 程序终止(interrupt)信号。
  • SIGSEGV 非法访问内存。
  • SIGTERM 发送到程序的终止请求。

signal() 函数:C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:

void (*signal (int sig, void (*func)(int)))(int);
// 上面看起来有点费劲,以下语法格式更容易理解:
signal(registered signal, signal handler)

这个函数接收两个参数:第一个参数是要设置的信号的标识符,第二个参数是指向信号处理函数的指针。函数返回值是一个指向先前信号处理函数的指针。如果先前没有设置信号处理函数,则返回值为 SIG_DFL。如果先前设置的信号处理函数为 SIG_IGN,则返回值为 SIG_IGN。

raise() 函数:使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:

int raise (signal sig);
// 在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP

多线程

内容与 C 语言相似,有问题时参考此处open in new window

资源库

STL 教程

C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。

C++ 标准模板库的核心包括以下三个组件:

  • 容器(Containers) 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。
  • 算法(Algorithms) 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。
  • 迭代器(iterators) 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。

小知识

c++ 的向量容器与数组很相似,不同点在于,向量在需要扩展内存时,会自动处理自己的存储需求。

标准库

两类:

  • 标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言(包含了所有 C 语言的标准库,为了支持类型安全,做了一定的添加和修改)。
  • 面向对象类库: 这个库是类及其相关函数的集合。

标准函数库:

  • 输入/输出 I/O
  • 字符串和字符处理
  • 数学
  • 时间、日期和本地化
  • 动态分配
  • 其他
  • 宽字符函数

面向对象类库:

  • 标准的 C++ I/O 类
  • String 类
  • 数值类
  • STL 容器类
  • STL 算法
  • STL 函数对象
  • STL 迭代器
  • STL 分配器
  • 本地化库
  • 异常处理类
  • 杂项支持库

结束

到此,CPP 的基础知识就学习结束了,本次学习是为了应对目前正在学习的现代 CPP 课程。有一个基础,这样上课也就能更好的跟上老师的节奏 🎃,并且还要应对课程项目。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.5