C++:重点解读
概念解释
关键概念:类&对象、基类、派生类、父类、子类、继承、抽象类、多态、虚函数、纯虚函数、实例化
类&对象
类
用于指定对象的形式,是一种用户自定义的数据类型,它是一个种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。
定义一个类的形式,如下:
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,它指向对象本身。
类的静态成员:类的成员变量和成员函数都可以被声明为静态的。
静态成员变量和静态成员函数的实现:
#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。
继承
允许我们依据另外一个类来定义一个类,这可以达到重用代码功能和提高执行效率的效果。
#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
说明这是一个虚函数,允许派生类重写它,如下:
#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;
}
抽象类和纯虚函数
实例化
是指创建一个类的具体实例(即对象)的过程。如下:
Dog dog("Rex"); // 创建了一个实例
如果类中至少有一个函数被声明为纯虚函数,则这个类就是 抽象类
。设计抽象类的目的,是为了给其他类提供一个可以继承的基类,抽象类不能用于实例化对象,它只能作为接口使用。另外,若派生类继承的基类是一个抽象类,那么基类中声明的 纯虚函数
,在派生类中要给出实现。如下:
#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 成员:只能被类自身的成员函数访问,即使是派生类也无法访问
#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)是为对象起的另一个名字,引用类型引用另外一种类型。
指针存放的是另外一个变量的地址,可以通过指针间接修改另外一个变量的值。
#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) 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
函数传参:传值和传引用
#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;
}
运行结果:
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 类及其派生类来处理异常。
首先,给出一个没有使用异常处理的例子:
#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;
}
运行结果:
结果: 5
Floating point exception (core dumped)
一个健壮性强的程序,绝对不能让异常导致程序本科,所以需要对异常进行处理,而不是让它崩溃。
改善后的例子:
#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;
}
运行结果:
结果: 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) 允许我们在声明函数时为某些参数提供默认值,这样在调用函数时可以省略这些参数,使用默认值来替代。
首先,给出一个例子来进行说明:
景区工作人员如何进行问候,对于未知姓名的人,直接都以游客称呼:"你好,游客"。而对于知道姓名的人,就使用名称称呼:"你好,张三"。
#include <iostream>
// 函数声明,带有默认参数
void greet(std::string name = "游客") {
std::cout << "你好, " << name << "!" << std::endl;
}
int main() {
greet(); // 没有传递参数,使用默认值
greet("张三"); // 传递了参数,覆盖默认值
return 0;
}
运行结果:
你好, 游客!
你好, 张三!
greet函数中的name就是默认实参。
默认参数的注意事项:
-
默认参数必须从右向左设置(即不能跳过中间的参数)。
-
可以在函数声明中提供默认参数,而不能在定义中重复提供。
-
默认参数可用于普通函数、构造函数和模板函数。
-
调用时,未提供的参数会使用默认值。
函数重载
函数重载指的是在同一个作用域中,多个函数名称相同,但参数列表(参数的数量、类型或顺序)不同。编译器会根据调用时提供的参数类型和个数来决定调用哪个函数。
首先,以一个例子来进行说明:
#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;
}
运行结果:
整型: 10
双精度浮点型: 3.14
字符串: Hello
add(3, 4) = 7
add(3, 4, 5) = 12
整型: 10, 双精度浮点型: 3.14
双精度浮点型: 3.14, 整型: 10
C++ 函数重载的规则:
- 函数名称相同,但参数不同(参数类型、参数个数、参数顺序)。
- 返回类型不参与重载区分(仅靠返回类型不同不能重载)。
- 默认参数可能导致二义性,谨慎使用。
命名空间
在 C++ 中,namespace
(命名空间)用于避免命名冲突,特别是在大型项目中,不同的库可能会定义相同的变量、函数或类。namespace
允许我们将相关的代码组织在一起,并提供作用域,防止名称冲突。
这里以现实生活中举例:假设这样一种情况,当一个学校上有两个名叫 Zara 的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的班级、他们的家庭住址,或者他们父母的名字等等。
#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;
}
运行结果:
我是 Alex,学号 1000,来自 一年级A班
我是 Alex,学号 1001,来自 一年级B班
我是 Alex,学号 1002,来自 二年级A班
智能指针
std::make_shared
shared_from_this()
类型转换
类型转换包括:隐式类型转换 和 强制类型转换。
例如:
int a = 100;
double b = a;
double b = static_cast<double>(a);