概念解释
关键概念:类&对象、基类、派生类、父类、子类、继承、抽象类、多态、虚函数、纯虚函数、实例化
类&对象
类
用于指定对象的形式,是一种用户自定义的数据类型,它是一个种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。
定义一个类的形式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Shape { public: 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(); MyClass obj; MyClass::staticFunction();
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); 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; };
class Dog : public Animal { public: using Animal::Animal;
virtual void speak() const override { std::cout << name << " says: Woof!" << std::endl; } };
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(); cat.speak();
return 0; }
|
抽象类和纯虚函数
实例化
是指创建一个类的具体实例(即对象)的过程。如下:
如果类中至少有一个函数被声明为纯虚函数,则这个类就是 抽象类
。设计抽象类的目的,是为了给其他类提供一个可以继承的基类,抽象类不能用于实例化对象,它只能作为接口使用。另外,若派生类继承的基类是一个抽象类,那么基类中声明的 纯虚函数
,在派生类中要给出实现。如下:
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; }
virtual void makeSound() const = 0;
protected: std::string name; };
class Dog : public Animal { public: using Animal::Animal;
void makeSound() const override { std::cout << name << " says: Woof!" << std::endl; } };
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(); cat.makeSound();
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>
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; };
class DerivedClass : public BaseClass { public: void accessBaseMembers() { protectedMethod();
std::cout << "Protected variable from BaseClass: " << protectedVar << std::endl; } };
int main() { BaseClass baseObj; baseObj.publicMethod();
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 = 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;
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;
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>
头文件中。这些异常类帮助开发者处理程序运行时可能出现的错误情况。以下是几个常用的异常类及其作用:
- std::exception:这是所有标准异常类的基类。它定义了一个名为
what()
的虚函数,该函数返回一个描述异常信息的 C 风格字符串。这个类通常不直接使用,而是被继承用于创建更具体的异常类型。
- std::bad_alloc:当通过操作符
new
分配内存失败时抛出。它是 std::exception
的派生类,表示内存分配错误。
- std::bad_cast:当使用
dynamic_cast
进行从多态基类到派生类的转换失败时抛出。它也是 std::exception
的派生类,但仅适用于 RTTI(Run-Time Type Information)相关的错误。
- std::bad_typeid:当对一个空指针使用
typeid
操作符时抛出。此异常类同样继承自 std::exception
,主要用于处理类型识别相关的错误。
- std::logic_error:这是一个逻辑错误异常的基类,表示可以在程序运行前检测到的错误。例如,违反了某个不变量或前提条件。其子类包括:
std::domain_error
:表示参数值不在函数定义域内。
std::invalid_argument
:表示传递给函数的参数无效。
std::length_error
:表示尝试生成过长的某种数据结构,如试图创建一个超出最大允许长度的容器。
std::out_of_range
:当访问容器(如数组、向量等)元素而索引超出范围时抛出。
- 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; }
|
运行结果:
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>
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; }
int add(int a, int b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
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(3.14); print("Hello");
std::cout << "add(3, 4) = " << add(3, 4) << std::endl; std::cout << "add(3, 4, 5) = " << add(3, 4, 5) << std::endl;
showInfo(10, 3.14); showInfo(3.14, 10);
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++ 函数重载的规则:
- 函数名称相同,但参数不同(参数类型、参数个数、参数顺序)。
- 返回类型不参与重载区分(仅靠返回类型不同不能重载)。
- 默认参数可能导致二义性,谨慎使用。
命名空间
在 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() { 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()