C++程序设计
1.基本语法与常用语句,面向对象的基本概念
基本结构、注释、数据类型、变量、常量、运算符及控制语句(if、switch、循环)和函数。
基本语法与常用语句
1.基本结构
C++ 程序的基本结构包括头文件、main()
函数和代码主体。每个C++程序的入口是 main()
函数。
#include <iostream> // 引入标准输入输出库
int main() {
std::cout << "Hello, World!" << std::endl; // 输出语句
return 0;
}
#include <iostream>
:引入库文件,用于标准输入输出。std::cout
:标准输出流,用于在屏幕上显示输出。std::endl
:表示换行。
2. 注释
C++ 支持单行和多行注释。
- 单行注释:
//
- 多行注释:
/* */
// 这是单行注释
/* 这是
多行注释 */
3. 数据类型
C++ 提供了多种数据类型,包括整型、浮点型、字符型等。
int a = 10; // 整型
float b = 3.14; // 浮点型
char c = 'A'; // 字符型
bool d = true; // 布尔型
4. 变量和常量
变量是可以改变的值,常量是不可改变的值。
int x = 5; // 变量
const int y = 10; // 常量
5. 运算符
C++ 支持各种运算符,包括算术、逻辑和关系运算符。
int sum = a + b; // 加法
bool result = (a == b); // 关系运算符
6. 控制语句
- 条件语句:
if-else
和switch
- 循环语句:
for
,while
,do-while
if (a > b) {
std::cout << "a is greater";
} else {
std::cout << "b is greater";
}
for (int i = 0; i < 5; i++) {
std::cout << i;
}
7. 函数
C++ 中函数是用于执行特定任务的独立模块。定义函数时,需要指定返回类型、函数名和参数列表。
int add(int num1, int num2) {
return num1 + num2;
}
面向对象的基本概念
C++ 是一门面向对象的编程语言,支持类和对象的概念。面向对象的四大基本特性是封装、继承、多态和抽象。
1. 类和对象
类是对象的蓝图,对象是类的实例。
class Car {
public:
string brand;
int year;
void start() {
std::cout << "Car started" << std::endl;
}
};
int main() {
Car myCar;
myCar.brand = "Toyota";
myCar.year = 2020;
myCar.start();
}
2. 封装
封装是将数据和操作数据的函数绑定在一起,并隐藏内部实现细节。类的成员可以是 private
、protected
或 public
,用于控制访问权限。
class Person {
private:
int age;
public:
void setAge(int a) {
age = a;
}
int getAge() {
return age;
}
};
3. 继承
继承是通过现有类创建新类的机制,派生类继承了基类的所有特性。
class Animal {
public:
void eat() {
std::cout << "Eating" << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "Barking" << std::endl;
}
};
4. 多态
多态性是指相同的函数可以有不同的行为,通常通过函数重载或虚函数实现。
class Animal {
public:
virtual void sound() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Woof" << std::endl;
}
};
5. 抽象
抽象是通过抽象类和接口来定义对象的通用行为。抽象类是不能实例化的类,通常包含至少一个纯虚函数。
class AbstractShape {
public:
virtual void draw() = 0; // 纯虚函数
};
class Circle : public AbstractShape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};
2.函数的调用,函数参数,函数的重载及函数作用域
1. 函数调用
函数调用是指在程序中使用已经定义好的函数。调用函数时,需要传递函数参数(如果有)并接收其返回值(如果函数有返回类型)。
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3); // 调用add函数
std::cout << "Result: " << result << std::endl;
return 0;
}
add(5, 3)
:这是函数调用,传递两个参数5
和3
。
2. 函数参数
函数参数可以分为两类:按值传递和按引用传递。
- 按值传递:函数接收到的是参数的副本,修改该副本不会影响原变量。
- 按引用传递:通过引用传递参数,函数内对参数的修改会直接影响原变量。
// 按值传递
void modifyValue(int a) {
a = 10; // 仅修改副本
}
// 按引用传递
void modifyReference(int &a) {
a = 10; // 直接修改原变量
}
int main() {
int x = 5;
modifyValue(x); // 传递副本,x 不变
std::cout << x << std::endl; // 输出:5
modifyReference(x); // 传递引用,x 被修改
std::cout << x << std::endl; // 输出:10
return 0;
}
- 默认参数:C++允许在函数声明中为参数提供默认值。如果调用函数时未传递这些参数,使用默认值。
int multiply(int a, int b = 2) {
return a * b;
}
int main() {
std::cout << multiply(5) << std::endl; // 输出:10,使用默认参数b=2
std::cout << multiply(5, 3) << std::endl; // 输出:15,传递参数b=3
return 0;
}
3. 函数重载
函数重载是指在同一个作用域中定义多个具有相同名称但不同参数列表的函数。编译器通过参数的数量和类型来确定调用哪个函数。
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int main() {
std::cout << add(5, 3) << std::endl; // 调用int版本,输出:8
std::cout << add(5.5, 3.3) << std::endl; // 调用double版本,输出:8.8
return 0;
}
- 函数的重载要求函数的参数列表必须不同,返回类型不同不能构成函数重载。
4. 函数作用域
函数的作用域决定了函数或变量的可见性和生命周期。C++ 中主要有以下几种作用域:
- 局部作用域:定义在函数内部的变量只能在函数内部使用。
- 全局作用域:定义在所有函数之外的变量可以在程序的任何地方访问。
- 类作用域:类中定义的成员函数或变量只能在类的对象或成员函数中使用。
int globalVar = 10; // 全局变量
void display() {
int localVar = 5; // 局部变量
std::cout << "Local: " << localVar << std::endl;
std::cout << "Global: " << globalVar << std::endl;
}
int main() {
display();
std::cout << "Global from main: " << globalVar << std::endl;
return 0;
}
- 局部变量遮蔽全局变量:当局部变量和全局变量同名时,局部变量会遮蔽全局变量,优先使用局部变量。
int var = 10; // 全局变量
void func() {
int var = 20; // 局部变量,遮蔽了全局变量
std::cout << var << std::endl; // 输出:20
}
int main() {
func();
std::cout << var << std::endl; // 输出:10,访问全局变量
return 0;
}
3.类的概念,类的定义与说明,类的成员函数,作用域
1. 类的概念
类是面向对象编程(OOP)中的核心概念,代表一种用户定义的数据类型,用来封装数据和操作这些数据的函数。类是对象的蓝图,而对象是类的实例。类允许代码更有结构和可重用性。
2. 类的定义与说明
一个类通常由数据成员(变量)和成员函数(方法)组成,这些成员函数用于对数据成员进行操作。定义类时,需要使用关键字 class
或 struct
。
class Car { // 定义类Car
public: // 公有访问修饰符
string brand;
int year;
void display() { // 成员函数
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
在 Car
类中,brand
和 year
是数据成员,display()
是一个成员函数。
public
关键字用于声明公有成员,可以在类外部访问。- 数据成员和成员函数可以有不同的访问级别(公有
public
、私有private
、保护protected
)。
3. 类的成员函数
成员函数是定义在类内部的函数,用于操作类的对象。可以在类定义内部直接声明成员函数,或者在类定义外部进行实现。
- 类内定义的成员函数:通常是简单的函数,直接在类内部定义。
- 类外定义的成员函数:先在类内声明函数,然后在类外使用
类名::成员函数名
的形式实现。
类内定义:
class Car {
public:
string brand;
int year;
void display() { // 在类内定义成员函数
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
类外定义:
class Car {
public:
string brand;
int year;
void display(); // 在类内声明成员函数
};
// 在类外实现成员函数
void Car::display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
4. 构造函数和析构函数
- 构造函数:类的构造函数是一个特殊的成员函数,用于在创建对象时初始化对象的成员变量。构造函数的名称与类名相同,不返回任何值。
class Car {
public:
string brand;
int year;
// 构造函数
Car(string b, int y) {
brand = b;
year = y;
}
void display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
在创建 Car
对象时,构造函数会自动被调用:
Car myCar("Toyota", 2020);
myCar.display(); // 输出:Brand: Toyota, Year: 2020
- 析构函数:析构函数是类的另一种特殊成员函数,用于在对象生命周期结束时执行清理操作。析构函数的名称与类名相同,前面加
~
符号。
class Car {
public:
~Car() { // 析构函数
std::cout << "Car object is being deleted" << std::endl;
}
};
5. 类的成员变量的作用域
成员变量的作用域指的是这些变量的可见性和使用范围,通常有以下三种访问修饰符:
- public(公有):公有成员可以在类外部访问。
- private(私有):私有成员只能在类的内部访问,不能在类外部直接使用。默认情况下,类的成员是私有的。
- protected(保护):保护成员只能在类的内部或派生类中访问,外部无法直接访问。
class Person {
private:
int age; // 私有成员变量
public:
string name; // 公有成员变量
// 公有成员函数
void setAge(int a) {
age = a;
}
int getAge() {
return age;
}
};
在上面的 Person
类中,name
是公有成员,外部可以直接访问;age
是私有成员,必须通过公有的 setAge()
和 getAge()
函数来访问和修改。
例:访问和操作成员变量
int main() {
Person person;
person.name = "John"; // 直接访问公有成员
person.setAge(25); // 使用公有成员函数设置私有成员
std::cout << person.name << std::endl; // 输出:John
std::cout << person.getAge() << std::endl; // 输出:25
}
- 类 是面向对象编程的核心,通过定义类可以封装数据和行为。
- 类的成员函数 可以在类内部或外部定义,用于操作类的对象。
- 构造函数和析构函数 是类的特殊成员函数,分别用于对象的初始化和销毁。
- 类的作用域 是通过
public
、private
和protected
访问修饰符来控制的,决定了类的成员在类外部和内部的可见性。
4.对象的概念,对象的初始化,对象的特殊生成方法,对象的生存期
1. 对象的概念
对象是类的实例,表示类定义的具体实现。通过类可以创建多个对象,每个对象都有自己独立的属性。类定义了对象的结构和行为,而对象则存储类中的数据并调用类的成员函数。
class Car {
public:
string brand;
int year;
void display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
int main() {
Car car1; // 创建对象car1
car1.brand = "Toyota";
car1.year = 2020;
car1.display(); // 调用对象的成员函数
}
在上述代码中,car1
是 Car
类的一个对象,它拥有 brand
和 year
属性,并通过 display()
函数来显示其属性。
2. 对象的初始化
对象的初始化有几种方式,最常见的是通过构造函数进行初始化。
- 通过构造函数初始化:构造函数是用于在对象创建时对其成员变量进行初始化的特殊函数。可以在定义对象时直接传递初始化参数。
class Car {
public:
string brand;
int year;
// 构造函数
Car(string b, int y) {
brand = b;
year = y;
}
void display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
int main() {
Car car1("Toyota", 2020); // 使用构造函数初始化对象
car1.display(); // 输出:Brand: Toyota, Year: 2020
}
-
默认构造函数:如果不定义构造函数,编译器会提供一个默认的构造函数。但默认构造函数不会对成员变量进行初始化。
-
初始化列表:构造函数也可以使用初始化列表来初始化成员变量,这种方式更加高效。
class Car {
public:
string brand;
int year;
// 使用初始化列表
Car(string b, int y) : brand(b), year(y) {}
void display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
3. 对象的特殊生成方法
除了构造函数,C++ 还提供了一些特殊方法来生成对象。
- 拷贝构造函数:用于创建一个新对象,该对象是另一个现有对象的副本。拷贝构造函数的参数是对象的引用。
class Car {
public:
string brand;
int year;
Car(string b, int y) : brand(b), year(y) {}
// 拷贝构造函数
Car(const Car &obj) {
brand = obj.brand;
year = obj.year;
}
void display() {
std::cout << "Brand: " << brand << ", Year: " << year << std::endl;
}
};
int main() {
Car car1("Toyota", 2020);
Car car2 = car1; // 使用拷贝构造函数
car2.display(); // 输出:Brand: Toyota, Year: 2020
}
- 动态分配对象:可以使用
new
运算符在堆上动态分配对象。这些对象必须手动释放,使用delete
关键字。
Car* car = new Car("Honda", 2022); // 动态创建对象
car->display(); // 使用箭头运算符调用成员函数
delete car; // 手动释放内存
4. 对象的生存期
C++ 中的对象生存期分为 自动存储期 和 动态存储期,取决于对象是在栈上还是在堆上分配。
- 自动存储期:栈上分配的对象具有自动存储期,当函数调用结束或对象离开其作用域时,自动销毁。栈上的对象不需要手动释放内存。
void createCar() {
Car car("BMW", 2021); // 栈上分配,函数结束时自动销毁
car.display();
}
- 动态存储期:使用
new
运算符在堆上创建的对象具有动态存储期,直到调用delete
释放内存时才会销毁。
void createCar() {
Car* car = new Car("Tesla", 2023); // 堆上分配
car->display();
delete car; // 必须手动释放内存
}
- 临时对象:有时在表达式中会产生临时对象,通常这些对象的生存期仅限于当前表达式的执行过程。例如,函数返回一个对象时,返回的对象会作为临时对象创建并在表达式结束后销毁。
5. 对象的析构函数
对象的析构函数用于在对象生存期结束时释放资源(如内存、文件等)。当对象离开作用域或被 delete
时,析构函数会被自动调用。
class Car {
public:
string brand;
// 构造函数
Car(string b) : brand(b) {
std::cout << brand << " is being created" << std::endl;
}
// 析构函数
~Car() {
std::cout << brand << " is being destroyed" << std::endl;
}
};
int main() {
Car car1("BMW"); // 构造函数被调用
// 在程序结束时,析构函数被调用
}
- 对象 是类的实例,通过类来创建和操作对象。
- 对象的初始化 可以通过构造函数来完成,使用默认构造、带参数的构造、初始化列表或拷贝构造函数。
- 特殊生成方法 包括拷贝构造函数和动态分配对象。
- 对象的生存期 取决于它们是栈上分配(自动生存期)还是堆上分配(动态生存期)。
- 析构函数 是对象生命周期结束时自动调用的函数,用于释放资源。
5.对象指针和对象引用,对象数组
在C++中,对象指针、对象引用和对象数组都是非常基础且重要的概念,理解它们有助于你更灵活地管理对象和内存。以下是对每个概念的详细介绍:
1. 对象指针(Pointer to Object)
对象指针是存储对象地址的指针,允许你通过指针间接访问对象。
定义和使用:
class MyClass {
public:
void display() {
std::cout << "Hello, Object!" << std::endl;
}
};
int main() {
MyClass obj; // 创建对象
MyClass* ptr = &obj; // 对象指针,指向对象
// 使用对象指针调用成员函数
ptr->display(); // 输出: Hello, Object!
return 0;
}
解释:
MyClass* ptr
定义了一个指向MyClass
对象的指针。&obj
获取obj
的地址,并将其赋值给ptr
。- 使用箭头符号
->
来通过指针调用对象的成员函数。
注意事项:
- 指针可以指向空地址(
nullptr
),需要在使用之前检查是否为nullptr
。 - 动态分配对象时,使用指针来管理堆内存中的对象,记得在使用完后手动释放内存(
delete
)。
2. 对象引用(Reference to Object)
对象引用是对象的别名,它提供了一种通过不同名称访问相同对象的方式。
定义和使用:
class MyClass {
public:
void display() {
std::cout << "Hello, Object!" << std::endl;
}
};
int main() {
MyClass obj; // 创建对象
MyClass& ref = obj; // 创建对象引用
// 使用引用调用成员函数
ref.display(); // 输出: Hello, Object!
return 0;
}
解释:
MyClass& ref = obj
定义了一个obj
对象的引用。- 通过引用
ref
可以直接访问obj
的成员函数或数据。
注意事项:
- 引用必须在定义时初始化,并且之后不能更改引用的对象。
- 引用不能为
nullptr
,它总是指向一个有效的对象。
3. 对象数组(Array of Objects)
对象数组是存储相同类型对象的连续内存空间,可以通过数组下标访问每个对象。
定义和使用:
class MyClass {
public:
void display() {
std::cout << "This is object!" << std::endl;
}
};
int main() {
MyClass objArr[3]; // 创建一个包含3个MyClass对象的数组
for(int i = 0; i < 3; i++) {
objArr[i].display(); // 输出: This is object!
}
return 0;
}
解释:
MyClass objArr[3];
定义了一个包含三个MyClass
对象的数组。- 通过下标访问数组中的每一个对象,并调用其成员函数。
注意事项:
-
数组的大小在定义时必须确定。
-
动态分配对象数组时,需要手动释放内存(
delete[]
)。 -
对象指针:可以指向对象并通过指针访问对象成员;适用于动态内存管理。
-
对象引用:对象的别名,提供了一种更安全的访问方式;适用于函数参数传递等场景。
-
对象数组:用于存储多个相同类型的对象,方便批量处理对象。
6.类的继承性和派生类
在C++中,类的继承性 和 派生类 是面向对象编程的重要特性,它们允许你基于已有的类(称为基类或父类)创建新的类(称为派生类或子类),从而重用代码并扩展功能。
1. 类的继承性(Inheritance)
继承性允许一个类继承另一个类的属性和方法,派生类可以扩展或修改从基类继承的功能。
基本语法:
class 基类名 {
public:
// 基类成员
};
class 派生类名 : 继承方式 基类名 {
public:
// 派生类新增或覆盖的成员
};
- 继承方式:可以是
public
、protected
或private
,影响基类成员在派生类中的可访问性。
继承方式的区别:
- public继承:基类的
public
成员在派生类中仍然是public
,基类的protected
成员在派生类中仍然是protected
。 - protected继承:基类的
public
和protected
成员都在派生类中变为protected
。 - private继承:基类的所有
public
和protected
成员在派生类中都变为private
。
2. 基类和派生类的使用
示例:
#include <iostream>
using namespace std;
class Animal {
public:
void speak() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal { // Dog类继承自Animal类
public:
void speak() {
cout << "Dog barks" << endl;
}
};
int main() {
Animal a;
Dog d;
a.speak(); // 输出: Animal makes a sound
d.speak(); // 输出: Dog barks
return 0;
}
解释:
Animal
是基类,它包含一个speak
方法。Dog
是从Animal
派生的类,继承了Animal
的成员,同时通过同名函数speak
覆盖了基类的方法。
注意:即使 Dog
类重写了 speak
方法,仍然可以通过指针或引用访问基类的 speak
方法,这就是覆盖与隐藏的概念。
3. 派生类中的构造函数与析构函数
在继承关系中,派生类的构造函数和析构函数有特殊的调用顺序:
- 构造函数:基类的构造函数会先于派生类的构造函数执行。
- 析构函数:派生类的析构函数先执行,然后是基类的析构函数。
示例:
class Base {
public:
Base() {
cout << "Base Constructor" << endl;
}
~Base() {
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "Derived Destructor" << endl;
}
};
int main() {
Derived obj;
// 输出:
// Base Constructor
// Derived Constructor
// Derived Destructor
// Base Destructor
return 0;
}
解释:
- 当创建
Derived
类对象时,首先调用基类Base
的构造函数,然后调用Derived
的构造函数。 - 当销毁对象时,派生类的析构函数先执行,然后是基类的析构函数。
4. 访问基类成员
派生类可以直接访问基类的 public
和 protected
成员。为了访问重载后的基类成员,可以使用作用域解析符。
示例:
class Base {
public:
void show() {
cout << "Base show" << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived show" << endl;
}
void callBaseShow() {
Base::show(); // 调用基类的show方法
}
};
int main() {
Derived obj;
obj.show(); // 输出: Derived show
obj.callBaseShow(); // 输出: Base show
return 0;
}
解释:
Derived::show()
覆盖了Base::show()
,但你可以使用Base::show()
来显式调用基类的版本。
5. 虚函数与多态(Virtual Functions and Polymorphism)
在继承中,如果想要派生类能够动态地调用自己的函数实现而不是基类的版本,通常需要用虚函数实现多态。
虚函数的定义:
class Base {
public:
virtual void show() { // 定义虚函数
cout << "Base show" << endl;
}
};
class Derived : public Base {
public:
void show() override { // 重写虚函数
cout << "Derived show" << endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->show(); // 输出: Derived show
delete ptr;
return 0;
}
解释:
-
Base::show()
是虚函数,允许通过基类指针或引用动态调用派生类的show()
方法。 -
Derived::show()
重写了基类的虚函数,当ptr
指向Derived
对象时,动态地调用了派生类的show()
函数。 -
继承性:使派生类能够重用基类的代码,同时扩展或修改功能。
-
派生类:继承了基类的属性和方法,可以重写方法或添加新的属性和方法。
-
虚函数与多态:通过虚函数实现动态绑定,使得派生类对象可以在运行时根据实际类型调用合适的方法。
7.虚基类与虚函数
在C++中,虚基类和虚函数是两个不同的概念,但都与继承机制和多态性相关。它们分别解决了类继承中的菱形继承问题和动态绑定问题。
1. 虚基类(Virtual Base Class)
虚基类主要用于解决菱形继承中的重复继承问题。
菱形继承问题:
当一个类同时继承自两个父类,而这两个父类又继承自同一个基类时,基类的成员会在最派生类中重复继承,导致数据冗余和混淆。这种结构称为菱形继承。
例子:
#include <iostream>
using namespace std;
class A {
public:
int value;
A() : value(10) {}
};
class B : public A {}; // B 继承自 A
class C : public A {}; // C 继承自 A
class D : public B, public C {}; // D 同时继承 B 和 C
int main() {
D obj;
// obj.value; // 编译错误,D 中存在两个 A::value
obj.B::value = 20; // 通过 B 访问 A::value
obj.C::value = 30; // 通过 C 访问 A::value
cout << "B's value: " << obj.B::value << endl; // 输出 20
cout << "C's value: " << obj.C::value << endl; // 输出 30
return 0;
}
问题:在类 D
中,由于 B
和 C
都继承自 A
,因此 A
的成员在 D
中存在两份。要访问 A::value
时,必须通过 B::value
或 C::value
来区分。
使用虚基类解决菱形继承问题:
为了解决这个重复继承的问题,C++提供了虚继承。当使用虚继承时,基类的成员只会存在一份,而不会被重复继承。
虚基类的例子:
#include <iostream>
using namespace std;
class A {
public:
int value;
A() : value(10) {}
};
class B : public virtual A {}; // 虚继承 A
class C : public virtual A {}; // 虚继承 A
class D : public B, public C {}; // D 同时继承 B 和 C
int main() {
D obj;
obj.value = 50; // 现在只有一份 A::value
cout << "Value: " << obj.value << endl; // 输出 50
return 0;
}
解释:
B
和C
虚继承自A
,这样当D
同时继承B
和C
时,A
的成员value
只会存在一份。- 可以直接通过
obj.value
访问A::value
,避免了重复继承导致的冲突。
2. 虚函数(Virtual Function)
虚函数是C++中实现多态性的关键,允许在运行时根据对象的实际类型调用派生类中的方法。
虚函数的作用:
在继承结构中,如果基类定义了一个虚函数,并且派生类重写了这个函数,使用基类指针或引用指向派生类对象时,虚函数机制会确保调用的是派生类的函数,而不是基类的函数。这种动态的函数调用机制称为动态绑定或运行时多态。
虚函数的定义和使用:
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { // 基类中的虚函数
cout << "Base show" << endl;
}
};
class Derived : public Base {
public:
void show() override { // 派生类重写虚函数
cout << "Derived show" << endl;
}
};
int main() {
Base* ptr = new Derived(); // 基类指针指向派生类对象
ptr->show(); // 输出: Derived show
delete ptr;
return 0;
}
解释:
Base::show()
被声明为虚函数(通过virtual
关键字)。- 在派生类
Derived
中,show()
函数重写了基类的虚函数。 - 在
main()
函数中,虽然ptr
是Base*
类型,但由于它指向一个Derived
对象,因此ptr->show()
会调用Derived
类的show()
,输出Derived show
。这就是多态的体现。
纯虚函数(Pure Virtual Function)与抽象类
如果基类的某个函数没有具体实现,只能在派生类中实现,那么可以将其定义为纯虚函数。包含纯虚函数的类称为抽象类,无法直接实例化,只能通过派生类来实例化。
纯虚函数的定义:
class Base {
public:
virtual void show() = 0; // 纯虚函数
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
};
int main() {
// Base b; // 错误:无法实例化抽象类
Derived d;
d.show(); // 输出: Derived show
Base* ptr = &d;
ptr->show(); // 输出: Derived show
return 0;
}
解释:
Base::show()
是纯虚函数,= 0
表示该函数没有实现。Derived
必须实现show()
,否则Derived
也将成为抽象类。- 不能实例化
Base
类,但可以通过派生类Derived
实例化。
3. 虚析构函数(Virtual Destructor)
在多态场景中,如果基类指针指向派生类对象,而你想正确地释放派生类对象的内存,那么基类的析构函数应该定义为虚析构函数。否则,当删除基类指针时,派生类的析构函数不会被调用,可能导致资源泄漏。
虚析构函数的例子:
class Base {
public:
virtual ~Base() { // 虚析构函数
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived Destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 输出: Derived Destructor
// Base Destructor
return 0;
}
解释:
-
Base
的析构函数被定义为虚函数,因此当基类指针ptr
被删除时,会先调用Derived
类的析构函数,然后再调用Base
类的析构函数,确保派生类对象的正确销毁。 -
虚基类:解决菱形继承中的重复继承问题,通过虚继承确保基类成员只存在一份。
-
虚函数:实现多态性,允许在运行时根据实际对象类型调用派生类的方法。
-
纯虚函数与抽象类:定义接口,强制派生类实现特定的方法。
-
虚析构函数:确保在多态场景下正确销毁派生类对象。
8.重载及其应用
函数重载(Function Overloading)是C++的一项重要特性,它允许在同一个作用域中定义多个同名的函数,只要它们的参数列表不同即可。这种特性使得代码更加简洁、易读,并且可以为不同类型或数量的输入提供不同的处理方式。
1. 重载的基本概念
在C++中,函数重载意味着同名函数根据参数的数量、参数类型或参数的排列顺序的不同而区分。编译器根据调用时的实际参数类型来选择调用哪个函数。
示例:
#include <iostream>
using namespace std;
void print(int i) {
cout << "Integer: " << i << endl;
}
void print(double d) {
cout << "Double: " << d << endl;
}
void print(string s) {
cout << "String: " << s << endl;
}
int main() {
print(10); // 输出: Integer: 10
print(10.5); // 输出: Double: 10.5
print("Hello"); // 输出: String: Hello
return 0;
}
解释:
- 函数
print()
重载了三次,分别处理int
、double
和string
类型的参数。 - 编译器根据传入的参数类型选择合适的函数进行调用。
2. 重载规则
C++中,函数重载的区分依据是参数的类型、数量或顺序,而返回值类型不作为重载的依据。因此,以下代码是不合法的,因为函数的参数列表是相同的,只是返回类型不同:
int add(int a, int b);
double add(int a, int b); // 错误:仅返回类型不同,无法重载
合法重载的情况:
-
参数类型不同:
void add(int a, double b); void add(double a, int b);
-
参数数量不同:
void add(int a); void add(int a, int b);
-
参数顺序不同:
void add(int a, double b); void add(double a, int b);
3. 应用场景
函数重载主要应用于以下场景:
(1)处理不同类型的输入
当相同的操作需要对不同的数据类型执行时,函数重载非常有用。例如,print
函数可以同时打印整数、浮点数和字符串。
(2)简化代码接口
通过重载,可以将多种类似功能组合成一个函数接口,提供更加统一的代码调用方式。例如,C++标准库中的 abs
函数就通过重载来处理不同类型的绝对值计算:
int abs(int n); // 处理整数
double abs(double n); // 处理双精度浮点数
(3)默认参数与重载结合
重载和默认参数可以结合使用,当函数定义了默认参数时,实际传递的参数数量可以少于函数参数的数量,而编译器根据传入参数的数量选择调用哪个重载版本。
示例:
#include <iostream>
using namespace std;
void greet(string name = "Guest") {
cout << "Hello, " << name << "!" << endl;
}
void greet(string firstName, string lastName) {
cout << "Hello, " << firstName << " " << lastName << "!" << endl;
}
int main() {
greet(); // 输出: Hello, Guest!
greet("John"); // 输出: Hello, John!
greet("John", "Doe"); // 输出: Hello, John Doe!
return 0;
}
解释:
- 函数
greet
被重载了两个版本,一个带有默认参数name
,另一个带有两个参数firstName
和lastName
。 - 根据传入的参数,编译器会选择合适的版本进行调用。
(4)运算符重载(Operator Overloading)
C++还允许运算符重载,即为自定义的类或数据类型定义新的操作符行为。运算符重载本质上也是函数重载,但它将运算符的操作扩展到类对象中。
运算符重载示例:
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
void display() {
cout << "(" << real << ", " << imag << "i)" << endl;
}
};
int main() {
Complex c1(1.0, 2.0);
Complex c2(2.0, 3.0);
Complex c3 = c1 + c2; // 使用 + 运算符重载
c3.display(); // 输出: (3.0, 5.0i)
return 0;
}
解释:
Complex
类重载了+
运算符,使得两个Complex
对象可以通过+
进行相加。operator+
是函数重载的一种特殊形式,它实现了运算符的自定义行为。
4. 模板与函数重载的结合
函数模板提供了一种更加灵活的方式来进行函数重载,尤其是在处理不同类型的数据时,可以用模板来避免重复定义类似的重载函数。
示例:
#include <iostream>
using namespace std;
template <typename T>
void display(T value) {
cout << "Value: " << value << endl;
}
int main() {
display(10); // 输出: Value: 10
display(10.5); // 输出: Value: 10.5
display("Hello"); // 输出: Value: Hello
return 0;
}
解释:
-
使用模板,
display
函数可以处理任何数据类型,编译器会根据传入参数的类型自动生成对应的函数。 -
这种方式在需要处理多种数据类型时比函数重载更加简洁。
-
函数重载:允许在同一作用域中定义多个同名函数,前提是它们的参数列表必须不同。函数重载提高了代码的灵活性和可读性。
-
应用场景:用于处理不同类型的输入、简化接口设计、运算符重载等。
-
运算符重载:是重载的一种特殊形式,用于为自定义类对象定义新的运算符行为。
-
模板结合重载:通过模板重载可以处理更多数据类型,使代码更加通用。
9.模版及其应用
C++ 模板(Templates)是泛型编程的重要工具,允许编写独立于数据类型的代码,使程序更具灵活性和可重用性。模板支持函数、类和别名模板,广泛应用于标准库(如 std::vector
、std::map
等)以及用户自定义的泛型代码。
1. 模板的基本概念
模板允许你在编写函数或类时,不必指定某个特定的类型,而是在使用时再指定具体的数据类型。主要有两类模板:
- 函数模板:定义一个通用函数,可以适用于不同的数据类型。
- 类模板:定义一个通用类,使其成员变量、函数等适用于不同的数据类型。
函数模板
函数模板用于编写能处理不同类型的函数,通常会通过模板参数指定数据类型。函数模板的基本语法如下:
template <typename T>
T add(T a, T b) {
return a + b;
}
- 这里的
typename T
或class T
声明了一个模板参数T
,它表示数据类型。在调用时,编译器会根据传入的参数推断出具体类型。 - 示例调用:
int result1 = add(10, 20); // T被推断为int
double result2 = add(3.14, 2.71); // T被推断为double
类模板
类模板允许你定义一个类,其中某些成员变量或成员函数可以使用不同的数据类型。其基本语法如下:
template <typename T>
class Box {
private:
T value;
public:
Box(T val) : value(val) {}
T getValue() const { return value; }
};
- 在实例化时,必须明确指定模板类型:
Box<int> intBox(10);
Box<double> doubleBox(3.14);
2. 模板特化与偏特化
在一些特定情况下,你可能需要对特定类型进行不同的实现,这时可以使用模板特化(Template Specialization)。
完全特化
完全特化是针对某一特定类型提供不同的实现。
template <>
class Box<bool> {
private:
bool value;
public:
Box(bool val) : value(val) {}
bool getValue() const { return value; }
};
偏特化
偏特化允许你针对某些模板参数提供特化实现,而不是全部参数。
template <typename T>
class Box<T*> {
private:
T* value;
public:
Box(T* val) : value(val) {}
T* getValue() const { return value; }
};
在这个例子中,模板被偏特化为指针类型 T*
。
3. 模板的应用
标准模板库(STL)
C++ 标准模板库广泛使用模板,为常用的数据结构(如容器)和算法提供泛型实现。常见的 STL 模板类包括:
- 容器类:如
std::vector<T>
、std::list<T>
、std::map<Key, T>
等,支持存储任意类型的数据。 - 算法:如
std::sort
、std::find
等,支持对任意容器类型的数据进行排序、查找等操作。
智能指针
C++11 引入的 std::shared_ptr<T>
和 std::unique_ptr<T>
使用了模板技术,以提供泛型的智能指针,帮助管理不同类型对象的生命周期,避免手动管理内存带来的问题。
通用编程与库
模板还广泛应用于各类库的实现,如 Boost 库、Eigen 矩阵库等,借助模板编写高效且通用的代码。
C++ 模板是实现泛型编程的核心工具,能够提升代码的复用性与灵活性。通过掌握模板函数、类模板,以及模板特化与偏特化等技术,开发者可以编写高效的通用代码,并充分利用 STL 和现代 C++ 的特性。
10.基本数据结构和算法的程序设计
C++中的基本数据结构和算法是程序设计的重要组成部分,它们帮助我们高效地存储、组织和处理数据。以下是常见的数据结构和相关的基本算法设计概述。
1. 数据结构概述
1.1 数组 (Array)
- 数组 是最基础的数据结构,存储一组相同类型的元素,具有固定大小。
- 特点:访问时间为常数( O ( 1 ) O(1) O(1)),插入和删除的时间复杂度为线性( O ( n ) O(n) O(n)),因为需要移动元素。
示例:
int arr[5] = {1, 2, 3, 4, 5};
1.2 链表 (Linked List)
- 链表 是一组节点,每个节点包含数据和指向下一个节点的指针。
- 特点:链表的插入和删除操作可以在常数时间内完成( O ( 1 ) O(1) O(1),若操作在头部或尾部),但随机访问需要线性时间( O ( n ) O(n) O(n))。
- 分类:单向链表、双向链表、循环链表。
示例(单向链表的节点):
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
1.3 栈 (Stack)
- 栈 是一种遵循“后进先出”(LIFO)原则的数据结构。元素只能从栈顶插入或删除。
- 操作:主要操作是
push
(入栈)和pop
(出栈)。
示例(使用 STL 实现栈):
#include <stack>
std::stack<int> s;
s.push(10); // 入栈
s.pop(); // 出栈
1.4 队列 (Queue)
- 队列 遵循“先进先出”(FIFO)原则,元素从队列尾部插入,从头部删除。
- 变种:双端队列(deque),可以从两端插入和删除。
示例(使用 STL 实现队列):
#include <queue>
std::queue<int> q;
q.push(10); // 入队
q.pop(); // 出队
1.5 树 (Tree)
- 树 是一种分层的非线性数据结构,由节点组成,每个节点有一个父节点和多个子节点。二叉树是最常见的形式,每个节点最多有两个子节点。
- 二叉搜索树 (BST):一种特殊的二叉树,满足每个节点左子树的值小于该节点,右子树的值大于该节点。
示例(二叉树节点定义):
struct TreeNode {
int data;
TreeNode* left;
TreeNode* right;
TreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};
1.6 图 (Graph)
- 图 是一组由边连接的节点。图可以是有向图或无向图,边可以带权重或不带权重。
- 表示方法:邻接矩阵和邻接表。
示例(邻接表表示图):
#include <vector>
std::vector<int> adj[5]; // 邻接表表示的无向图
adj[0].push_back(1);
adj[1].push_back(0);
2. 基本算法设计
2.1 排序算法
- 冒泡排序:通过不断交换相邻元素,使较大的元素逐渐上浮。时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
示例(冒泡排序):
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n-1; ++i) {
for (int j = 0; j < n-i-1; ++j) {
if (arr[j] > arr[j+1]) {
std::swap(arr[j], arr[j+1]);
}
}
}
}
- 快速排序 (QuickSort):通过选择一个“基准元素”将数组分成两部分,递归排序。平均时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)。
示例(快速排序):
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (arr[j] < pivot) {
++i;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return i + 1;
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
2.2 搜索算法
- 线性搜索:从头到尾遍历元素,找到目标值。时间复杂度为 O ( n ) O(n) O(n)。
- 二分搜索:适用于已排序数组,通过不断折半查找。时间复杂度为 O ( log n ) O(\log n) O(logn)。
示例(二分搜索):
int binarySearch(int arr[], int low, int high, int target) {
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == target)
return mid;
if (arr[mid] < target)
low = mid + 1;
else
high = mid - 1;
}
return -1; // 未找到
}
2.3 贪心算法 (Greedy Algorithm)
- 贪心算法通过每一步选择局部最优解,最终希望获得全局最优解。
示例(找零问题):
int minCoins(int coins[], int n, int amount) {
int count = 0;
for (int i = n-1; i >= 0; --i) {
while (amount >= coins[i]) {
amount -= coins[i];
count++;
}
}
return count;
}
2.4 动态规划 (Dynamic Programming)
- 动态规划通过将问题分解为子问题,利用子问题的解构建最终解。它适用于有重叠子问题的情况。
示例(斐波那契数列):
int fib(int n) {
if (n <= 1) return n;
int dp[n+1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
掌握数据结构和算法是编写高效程序的关键。通过了解数组、链表、树、图等基本数据结构,及其常用的算法如排序、搜索、贪心和动态规划,能够提升你编写高效代码的能力。在实际开发中,选择合适的数据结构和算法是解决问题的核心。