C++:重点解读

概念解释

关键概念:类&对象、基类、派生类、父类、子类、继承、抽象类、多态、虚函数、纯虚函数、实例化

类&对象

用于指定对象的形式,是一种用户自定义的数据类型,它是一个种封装了数据函数的组合。类中的数据称为成员变量,函数称为成员函数

定义一个类的形式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Shape 
{
public: // 访问修饰符:private/public/protected,默认情况下是定义为 private
void setWidth(int w) // 成员函数
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width; // 成员变量
int height;
private:

};

构造函数:类的构造函数是一种特殊的函数,在创建(或实例化)一个新对象时调用。

析构函数:类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。

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

友元函数:类的友元函数可以访问类的 private 和 protected 成员。

内联函数:通过内联函数,编译器可以在调用该函数的地方扩展函数体中的代码。

this 指针:每一个对象都有一个特殊的指针 this,它指向对象本身。

类的静态成员:类的成员变量和成员函数都可以被声明为静态的。

静态成员变量和静态成员函数的实现:

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

class MyClass {
public:
// 静态成员函数的声明
static void staticFunction();

private:
static int count; // 静态成员变量
};

// 静态成员变量的定义和初始化
int MyClass::count = 0;

// 静态成员函数的定义
void MyClass::staticFunction() {
++count;
std::cout << "Count: " << count << std::endl;
}

int main() {
// 调用静态成员函数,无需创建对象
MyClass::staticFunction(); // 输出: Count: 1

MyClass obj;
MyClass::staticFunction(); // 输出: Count: 2

return 0;
}

静态成员变量属于类本身,而是类的某个特定实例,这意味着所有类的对象共享同一个静态成员变量。无论创建了多少个类的实例,静态成员变量只存在一份副本。

基类和派生类

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

基类也称为父类;派生类也称为子类。

例如:定义了一个基类:class Shape,可以 继承 基类来定义一个派生类:class Rectangle。

继承 允许我们依据另外一个类来定义一个类,这可以达到重用代码功能和提高执行效率的效果。

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
41
42
43
#include <iostream>

using namespace std;

// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};

// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};

int main(void)
{
Rectangle Rect;

Rect.setWidth(5);
Rect.setHeight(7);

// 输出对象的面积: "Total area: 35"
cout << "Total area: " << Rect.getArea() << endl;

return 0;
}

注意:

1、如果不定义任何构造函数,编译器会为你的类生成一个默认构造函数。

2、如果不定义任何析构函数,编译器会为你的类生成一个默认析构函数

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

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

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

多态

当类之间存在层次结构,并且类之间是通过继承关联,就会用到多态。C++多态允许使用基类指针或引用类调用子类的重写方法,从而使得同一个接口(方法)可以表现为不同的行为。

使用虚函数可以实现多态,在基类的函数(方法)加上前缀 virtual 说明这是一个虚函数,允许派生类重写它,如下:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <string>

// 基类(父类)
class Animal {
public:
// 构造函数
Animal(const std::string& name) : name(name) {}

// 虚析构函数
~Animal() {
std::cout << "Animal destructor called for " << name << std::endl;
}

// 虚函数,允许在派生类中重写
virtual void speak() const {
std::cout << "Some generic animal sound" << std::endl;
}

protected:
std::string name; // 动物的名字
};

// 派生类(子类)Dog
class Dog : public Animal {
public:
using Animal::Animal; // 继承基类构造函数

// 重写基类的方法
virtual void speak() const override {
std::cout << name << " says: Woof!" << std::endl;
}
};

// 派生类(子类)Cat
class Cat : public Animal {
public:
using Animal::Animal; // 继承基类构造函数

// 重写基类的方法
virtual void speak() const override {
std::cout << name << " says: Meow!" << std::endl;
}
};

int main() {
Dog dog("Rex");
Cat cat("Misty");

dog.speak(); // 输出: Rex says: Woof!
cat.speak(); // 输出: Misty says: Meow!

// 当main函数结束时,dog和cat对象会被销毁,它们的析构函数会被调用。
return 0;
}

抽象类和纯虚函数

实例化 是指创建一个类的具体实例(即对象)的过程。如下:

1
Dog dog("Rex"); // 创建了一个实例

如果类中至少有一个函数被声明为纯虚函数,则这个类就是 抽象类。设计抽象类的目的,是为了给其他类提供一个可以继承的基类,抽象类不能用于实例化对象,它只能作为接口使用。另外,若派生类继承的基类是一个抽象类,那么基类中声明的 纯虚函数 ,在派生类中要给出实现。如下:

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
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <string>

// 基类(父类)
class Animal {
public:
// 构造函数
Animal(const std::string& name) : name(name) {}

// 虚析构函数
~Animal() {
std::cout << "Animal destructor called for " << name << std::endl;
}

// 纯虚函数 makeSound
virtual void makeSound() const = 0;

protected:
std::string name; // 动物的名字
};

// 派生类(子类)Dog
class Dog : public Animal {
public:
using Animal::Animal; // 继承基类构造函数

void makeSound() const override {
std::cout << name << " says: Woof!" << std::endl;
}
};

// 派生类(子类)Cat
class Cat : public Animal {
public:
using Animal::Animal; // 继承基类构造函数

void makeSound() const override {
std::cout << name << " says: Meow!" << std::endl;
}
};

int main() {
Dog dog("Rex");
Cat cat("Misty");

dog.makeSound(); // 输出: Rex says: Woof!
cat.makeSound(); // 输出: Misty says: Meow!

// 当main函数结束时,dog和cat对象会被销毁,先销毁cat,再销毁dog,它们的析构函数会被调用。
return 0;
}

类的特性

  • 抽象:隐藏复杂的实现细节,仅暴露必要的部分给用户。
  • 封装:将数据和操作数据的方法捆绑在一起,并通过访问修饰符(public、private、protected)来控制对类内部数据的访问级别。
  • 多态:允许统一接口表示不同类型的对象。
  • 继承:允许一个类从另外一个类继承数据和操作数据的方法,从而实现代码重用和扩展功能。

结构体和类

在C++中,结构体(struct)与类(class)非常相似,实际上它们之间唯一的默认差异在于成员的访问权限:

  • 结构体的成员默认是公有的(public
  • 类的成员默认是私有的(private)。

访问修饰符

  • public 成员:任何地方都可以访问
  • protected 成员:只能被类自身及其派生类访问
  • private 成员:只能被类自身的成员函数访问,即使是派生类也无法访问
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>

// 基类 BaseClass
class BaseClass {
public:
// 公有成员函数
void publicMethod() {
std::cout << "This is a public method in BaseClass." << std::endl;
protectedMethod(); // 可以调用受保护的成员函数
accessPrivate(); // 可以调用私有的成员函数(在同一类中)
}

protected:
// 受保护成员函数
void protectedMethod() {
std::cout << "This is a protected method in BaseClass." << std::endl;
}

int protectedVar = 10; // 受保护的数据成员

private:
// 私有成员函数
void accessPrivate() {
std::cout << "This is a private method in BaseClass." << std::endl;
}

int privateVar = 20; // 私有的数据成员
};

// 派生类 DerivedClass 继承自 BaseClass
class DerivedClass : public BaseClass {
public:
void accessBaseMembers() {
// publicMethod(); // 可以调用基类的公有方法
protectedMethod(); // 可以调用基类的受保护方法
// accessPrivate(); // 错误:不能访问基类的私有方法
// std::cout << privateVar << std::endl; // 错误:不能访问基类的私有数据成员

std::cout << "Protected variable from BaseClass: " << protectedVar << std::endl; // 可以访问基类的受保护数据成员
}
};

int main() {
BaseClass baseObj;
baseObj.publicMethod(); // 正确:可以从外部调用公有方法
// baseObj.protectedMethod(); // 错误:不能从外部调用受保护的方法
// std::cout << baseObj.protectedVar << std::endl; // 错误:不能从外部访问受保护的数据成员
// baseObj.accessPrivate(); // 错误:不能从外部调用私有方法
// std::cout << baseObj.privateVar << std::endl; // 错误:不能从外部访问私有的数据成员

DerivedClass derivedObj;
derivedObj.publicMethod(); // 正确:派生类可以调用基类的公有方法
derivedObj.accessBaseMembers(); // 正确:派生类可以访问基类的受保护成员

return 0;
}

引用和指针

引用(reference)是为对象起的另一个名字,引用类型引用另外一种类型。

指针存放的是另外一个变量的地址,可以通过指针间接修改另外一个变量的值。

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

int main(int argc, char *argv[]) {
int a = 10;
int *p_a = &a; // 指针
int &x = a; // 引用

std::cout << "1) print a's value:\n";
std::cout << "a: " << a << std::endl;
std::cout << "*p_a: " << *p_a << std::endl;
std::cout << "x: " << x << std::endl;

// 修改a的值
a = 20;
std::cout << "2) print a's value:\n";
std::cout << "a: " << a << std::endl;
std::cout << "*p_a: " << *p_a << std::endl;
std::cout << "x: " << x << std::endl;

// 通过指针修改a的值
std::cout << "3) print a's value:\n";
*p_a = 30;
std::cout << "a: " << a << std::endl;
std::cout << "*p_a: " << *p_a << std::endl;
std::cout << "x: " << x << std::endl;

// 通过引用修改a的值
std::cout << "4) print a's value:\n";
x = 40;
std::cout << "a: " << a << std::endl;
std::cout << "*p_a: " << *p_a << std::endl;
std::cout << "x: " << x << std::endl;

return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1) print a's value:
a: 10
*p_a: 10
x: 10
2) print a's value:
a: 20
*p_a: 20
x: 20
3) print a's value:
a: 30
*p_a: 30
x: 30
4) print a's value:
a: 40
*p_a: 40
x: 40

函数传参:传值和传引用

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

/* 传值 */
void swap_1(int x, int y) {
int temp = x;
x = y;
y = temp;
}

void swap_2(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}

/* 传引用 */
void swap_3(int &x, int &y) {
int temp = x;
x = y;
y = temp;
}

int main(int argc, char *argv[]) {
int a = 3, b = 5;

std::cout << "a: " << a << ", b: " << b << std::endl;

std::cout << "1) swap_1(int x , int y): \n";
swap_1(a, b);
std::cout << "a: " << a << ", b: " << b << std::endl;

std::cout << "1) swap_2(int *x , int *y): \n";
swap_2(&a, &b);
std::cout << "a: " << a << ", b: " << b << std::endl;

std::cout << "1) swap_3(int &x , int &y): \n";
swap_3(a, b);
std::cout << "a: " << a << ", b: " << b << std::endl;

return 0;
}

运行结果:

1
2
3
4
5
6
7
a: 3, b: 5
1) swap_1(int x , int y):
a: 3, b: 5
1) swap_2(int *x , int *y):
a: 5, b: 3
1) swap_3(int &x , int &y):
a: 5, b: 3

异常处理

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。

在 C++ 编程中,异常处理是一种重要的错误处理机制,它允许程序在遇到错误时,能够优雅地处理这些错误,而不是让程序崩溃。

在 C++ 中,异常处理通常使用 try、catch 和 throw 关键字来实现。标准库中提供了 std::exception 类及其派生类来处理异常。

首先,给出一个没有使用异常处理的例子:

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

void divide(int a, int b) {
std::cout << "结果: " << a / b << std::endl;
}

int main() {
divide(10, 2);
divide(10, 0); // 这里造成程序崩溃

std::cout << "程序继续运行..." << std::endl;
return 0;
}

运行结果:

1
2
结果: 5
Floating point exception (core dumped)

一个健壮性强的程序,绝对不能让异常导致程序本科,所以需要对异常进行处理,而不是让它崩溃。

改善后的例子:

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

void divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("除数不能为零!");
}
std::cout << "结果: " << a / b << std::endl;
}

int main() {
try {
divide(10, 2);
divide(10, 0); // 这里会抛出异常
} catch (const std::exception& e) {
std::cerr << "捕获到异常: " << e.what() << std::endl;
}

std::cout << "程序继续运行..." << std::endl;
return 0;
}

运行结果:

1
2
3
结果: 5
捕获到异常: 除数不能为零!
程序继续运行...

在 C++ 中,标准库提供了一系列的异常类,它们位于 <exception> 头文件中。这些异常类帮助开发者处理程序运行时可能出现的错误情况。以下是几个常用的异常类及其作用:

  1. std::exception:这是所有标准异常类的基类。它定义了一个名为 what() 的虚函数,该函数返回一个描述异常信息的 C 风格字符串。这个类通常不直接使用,而是被继承用于创建更具体的异常类型。
  2. std::bad_alloc:当通过操作符 new 分配内存失败时抛出。它是 std::exception 的派生类,表示内存分配错误。
  3. std::bad_cast:当使用 dynamic_cast 进行从多态基类到派生类的转换失败时抛出。它也是 std::exception 的派生类,但仅适用于 RTTI(Run-Time Type Information)相关的错误。
  4. std::bad_typeid:当对一个空指针使用 typeid 操作符时抛出。此异常类同样继承自 std::exception,主要用于处理类型识别相关的错误。
  5. std::logic_error:这是一个逻辑错误异常的基类,表示可以在程序运行前检测到的错误。例如,违反了某个不变量或前提条件。其子类包括:
    • std::domain_error:表示参数值不在函数定义域内。
    • std::invalid_argument:表示传递给函数的参数无效。
    • std::length_error:表示尝试生成过长的某种数据结构,如试图创建一个超出最大允许长度的容器。
    • std::out_of_range:当访问容器(如数组、向量等)元素而索引超出范围时抛出。
  6. std::runtime_error:这是运行时错误异常的基类,表示只能在程序运行期间检测到的错误。其子类包括:
    • std::range_error:表示计算结果超出了有意义的值范围。
    • std::overflow_error:表示算术溢出错误。
    • std::underflow_error:表示算术下溢错误,尽管在实际应用中很少使用。

这些异常类可以帮助程序员更好地理解和处理程序中的错误情况,通过捕获特定类型的异常,可以进行针对性的错误恢复或用户通知。使用时,通常会将可能抛出异常的操作包含在 try 块中,并用相应的 catch 块来捕获和处理异常。

函数默认参数

在 C++ 中,默认实参(Default Arguments) 允许我们在声明函数时为某些参数提供默认值,这样在调用函数时可以省略这些参数,使用默认值来替代。

首先,给出一个例子来进行说明:

景区工作人员如何进行问候,对于未知姓名的人,直接都以游客称呼:”你好,游客”。而对于知道姓名的人,就使用名称称呼:”你好,张三”。

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

// 函数声明,带有默认参数
void greet(std::string name = "游客") {
std::cout << "你好, " << name << "!" << std::endl;
}

int main() {
greet(); // 没有传递参数,使用默认值
greet("张三"); // 传递了参数,覆盖默认值

return 0;
}

运行结果:

1
2
你好, 游客!
你好, 张三!

greet函数中的name就是默认实参。

默认参数的注意事项:

  • 默认参数必须从右向左设置(即不能跳过中间的参数)。

  • 可以在函数声明中提供默认参数,而不能在定义中重复提供

  • 默认参数可用于普通函数、构造函数和模板函数

  • 调用时,未提供的参数会使用默认值

函数重载

函数重载指的是在同一个作用域中,多个函数名称相同,但参数列表(参数的数量、类型或顺序)不同。编译器会根据调用时提供的参数类型和个数来决定调用哪个函数。

首先,以一个例子来进行说明:

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
41
42
43
44
45
46
47
#include <iostream>

// 重载的 print 函数,参数类型不同
void print(int i) {
std::cout << "整型: " << i << std::endl;
}

void print(double d) {
std::cout << "双精度浮点型: " << d << std::endl;
}

void print(std::string s) {
std::cout << "字符串: " << s << std::endl;
}

// 重载 add 函数,参数个数不同
int add(int a, int b) {
return a + b;
}

int add(int a, int b, int c) {
return a + b + c;
}

// 重载 showInfo 函数,参数顺序不同
void showInfo(int a, double b) {
std::cout << "整型: " << a << ", 双精度浮点型: " << b << std::endl;
}

void showInfo(double b, int a) {
std::cout << "双精度浮点型: " << b << ", 整型: " << a << std::endl;
}

int main() {
print(10); // 调用 print(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(std::string)

std::cout << "add(3, 4) = " << add(3, 4) << std::endl; // 调用 add(int, int)
std::cout << "add(3, 4, 5) = " << add(3, 4, 5) << std::endl; // 调用 add(int, int, int)

showInfo(10, 3.14); // 调用 showInfo(int, double)
showInfo(3.14, 10); // 调用 showInfo(double, int)

return 0;
}

运行结果:

1
2
3
4
5
6
7
整型: 10
双精度浮点型: 3.14
字符串: Hello
add(3, 4) = 7
add(3, 4, 5) = 12
整型: 10, 双精度浮点型: 3.14
双精度浮点型: 3.14, 整型: 10

C++ 函数重载的规则:

  1. 函数名称相同,但参数不同(参数类型、参数个数、参数顺序)。
  2. 返回类型不参与重载区分(仅靠返回类型不同不能重载)。
  3. 默认参数可能导致二义性,谨慎使用

命名空间

在 C++ 中,namespace(命名空间)用于避免命名冲突,特别是在大型项目中,不同的库可能会定义相同的变量、函数或类。namespace 允许我们将相关的代码组织在一起,并提供作用域,防止名称冲突。

这里以现实生活中举例:假设这样一种情况,当一个学校上有两个名叫 Zara 的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的班级、他们的家庭住址,或者他们父母的名字等等。

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
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>

namespace Grade1 {
namespace ClassA {
struct Student {
std::string name;
int id;

void introduce() {
std::cout << "我是 " << name << ",学号 " << id << ",来自 一年级A班" << std::endl;
}
};
}
namespace ClassB {
struct Student {
std::string name;
int id;

void introduce() {
std::cout << "我是 " << name << ",学号 " << id << ",来自 一年级B班" << std::endl;
}
};
}
}

namespace Grade2 {
namespace ClassA {
struct Student {
std::string name;
int id;

void introduce() {
std::cout << "我是 " << name << ",学号 " << id << ",来自 二年级A班" << std::endl;
}
};
}
}

int main() {
// 访问不同的 Alex
Grade1::ClassA::Student stu1 = {"Alex", 1000};
stu1.introduce();

Grade1::ClassB::Student stu2 = {"Alex", 1001};
stu2.introduce();

Grade2::ClassA::Student stu3 = {"Alex", 1002};
stu3.introduce();

return 0;
}

运行结果:

1
2
3
我是 Alex,学号 1000,来自 一年级A班
我是 Alex,学号 1001,来自 一年级B班
我是 Alex,学号 1002,来自 二年级A班

智能指针

std::make_shared

shared_from_this()