c++课堂笔记
写在前面:这是针对2024春cau《面向对象的程序设计》以及我个人的学习积累整理的笔记,其中大纲的整理受到了https://blog.csdn.net/chenlong_cxy/article/details/127166206 的启发
(一)c++基础
1.1数据类型
相比于c多了一个bool(基本数据类型) ## 1.2命名空间 在C++编程中,命名空间是一种特性,用于将代码进行逻辑分组,以避免命名冲突和组织代码。命名空间可以被看作是一个容器,它封装了一组相关的类、函数、变量等,为它们提供了一个独立的命名空间域,从而可以避免在全局命名空间中因名称重复而导致的冲突。
命名空间的作用: 避免命名冲突:在大型项目中,不同的库或模块可能使用相同的函数名或变量名。通过将这些名称放在不同的命名空间中,可以确保它们不会发生冲突。
代码组织:命名空间有助于将相关的代码组织在一起,使得代码结构更清晰,更易于维护。
提高可读性:通过使用有意义的命名空间名称,可以使得代码更加易于理解。例如,一个名为MathFunctions的命名空间可能包含与数学计算相关的函数。
我们常用的库称为c++标准库(stl) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
--------------------------------
#include <iostream>
#include <vector>
using namespace std;
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
std::cout << num << " ";
}
cout << std::endl;
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
24namespace MyNamespace {
int myVariable = 10;
void myFunction() {
// 函数实现
}
}
using MyNamespace::myFunction;
int main() {
myFunction(); // 直接调用函数
return 0;
}
----------------------------------------------------
#include <iostream>
namespace MyMath {
int add(int a, int b) {
return a + b;
}
}
int main() {
// 使用完全限定名调用函数
std::cout << "Sum: " << MyMath::add(5, 7) << std::endl;
return 0;
}
c++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,在调用函数的时候,如果不写相应位置的参数,则调用的参数就为缺省值。
1
2
3void fun(int a, int b = 1, int c = 2) {
cout << "a=" << a << "\tb=" << b << "\tc=" << c << endl;
}
不过函数的赋值是从左到右的
1.4函数重载
所谓函数重载,就是说同一个函数名,如果接收参数不同,那么就不属于命名冲突,调用时会自动判断传入的参数来决定使用哪一个函数
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>
using namespace std;
int sumOfSquare(int x, int y);
double sumOfSquare(double x, double y);
int main()
{
int x1, y1;
double x2, y2;
cout<<"Enter two integer:"<<endl;
cin>>x1>>y1;
cout<<"Their sum of square = "<<sumOfSquare(x1, y1)<<endl;
cout<<"Enter two Real Number:"<<endl;
cin>>x2>>y2;
cout<<"Their sum of square = "<<sumOfSquare(x2, y2)<<endl;
return 0;
}
int sumOfSquare(int x1, int y1)
{
return (x1*x1+y1*y1);
}
double sumOfSquare(double x2, double y2)
{
return (x2*x2+y2*y2);
}
ati:参数的默认值必须在函数声明中指定!!!
1.5引用
1.5.1引用的定义:
1引用是一个变量的别名,也就是说,它是某个已存在变量的另一个名字。
2一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
1.5.2引用与指针的区别:
1不存在空引用。引用必须连接到一块合法的内存。一旦引用被初始化为一个对象,就不能被指向到另一个对象,指针可以在任何时候指向到另一个对象。(引用自带const属性)
C++ 标准不允许引用重新指向其他对象,这相当于引用具有内置的 const
属性(但不是指引用的对象本身是 const,而是指引用的指向是 const)。
1
2
3
4
5
6
7
8
9
10
11int main() {
int x = 5;
int &ref = x; // 引用被初始化为x,连接到x所占用的合法内存
// ref现在引用x,且不能改变其引用的对象
// 以下代码是非法的,因为C++中的引用不能在创建后改变其指向的对象
// int y = 10;
// ref = y; // 错误!C++中的引用不能在创建后重新指向其他对象
return 0;
}
2引用必须在创建时被初始化,而指针可以在任何时间被初始化。
1 |
|
1.5.3创建引用:
假设变量名称是变量附属在内存位置中的标签,我们可以把引用当成是变量附属在内存位置中的第二个标签。
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
26int i = 17;
int& r = i; // 创建一个整型引用 r,它是 i 的别名
r = 42; // 修改 r,实际上也修改了 i
cout << "i 的值:" << i << endl; // 输出 42
#include <iostream>
using namespace std;
// 交换两个整数的值
void swapIntegers(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 10, b = 20;
cout << "Before swapping: a = " << a << ", b = " << b << endl;
swapIntegers(a, b);
cout << "After swapping: a = " << a << ", b = " << b << endl;
return 0;
}
1.5.4常量引用和值传递的内存分析
值传递(Pass by Value)和常量引用(Constant Reference)是编程中参数传递的两种方式,它们在内存中的表现和行为有所不同。下面我将结合内存来详细解释这两种概念。
值传递(Pass by Value)
在值传递中,函数的参数是通过复制实际参数的值来创建的。这意味着在内存中会为这些参数分配新的空间,并将实际参数的值复制到这些新分配的空间中。因此,函数内部对参数的任何修改都不会影响到实际参数。
以C++为例,如果你有一个函数void func(int
x),并且在调用这个函数时传入了一个整数变量int a = 5;
func(a);,那么在调用func时,会在栈上为新变量x分配空间,并将a的值(即5)复制到x中。如果函数func修改了x的值,这个修改不会反映到a上,因为x和a是两个不同的内存位置中的值。
常量引用(Constant Reference)
常量引用是一种特殊的引用类型,它允许你通过引用来传递参数,但不允许在函数内部修改引用的值。在内存中,常量引用并不创建参数的副本,而是直接使用了实际参数的内存地址。这意味着常量引用提供了一种效率更高的参数传递方式,因为它避免了复制数据。
继续上面的例子,如果你将函数改为void func(const int& x)并使用相同的调用方式func(a),那么在调用func时,不会为x分配新的内存空间。相反,x会直接引用a的内存地址。由于x是一个常量引用,所以你不能在func内部修改x的值(尝试这样做会导致编译错误)。但是,你可以读取x的值,它将是a的值(即5)。
常量引用的好处之一是它可以避免不必要的复制操作,特别是当处理大型对象或数据结构时,这可以显著提高性能。另一个好处是它可以处理不可修改的数据或需要保护的数据,确保这些数据在函数内部不会被意外修改。
总的来说,值传递和常量引用在内存中的表现和行为有所不同。值传递会创建参数的副本并分配新的内存空间,而常量引用则直接使用实际参数的内存地址并禁止修改引用的值。选择哪种传递方式取决于你的具体需求和程序的上下文。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <iostream>
#include <string>
using namespace std;
// 定义一个Person类
class Person {
private:
string name; // 私有数据成员:姓名
int age; // 私有数据成员:年龄
public:
// 公共成员函数:构造函数,用于初始化对象
Person(const string& n, int a) : name(n), age(a) {}
// 也可以采用值传递
// Person(string n,int a) : name(n),age(a) {}
}
(二)类和对象
2.1类的基本概念
2.1.1类的定义
1 |
|
2.1.2类的封装
1 |
|
2.1.3this指针
1 |
|
2.1.4类的初始化列表
1 |
|
2.2构造函数与析构函数
2.2.1构造函数
构造函数是一种特殊的成员函数,它在创建类的对象时被自动调用,用于初始化对象的状态。构造函数的名字与类的名称相同,且没有返回类型,甚至连void也没有。它的主要任务是初始化对象的成员变量或执行一些在创建对象时需要进行的操作。
构造函数可以根据需要设置参数,这些参数用于初始化对象的状态。当创建类的对象时,可以通过构造函数传递参数来初始化对象的属性。如果没有提供显式的构造函数,编译器会自动生成一个默认的无参构造函数。但是,如果类中定义了任何一个构造函数(无论是有参还是无参),编译器就不会自动生成默认构造函数。
1 |
|
2.2.2析构函数
在C++中,~B() 通常表示类 B 的析构函数的声明。析构函数是一个特殊的成员函数,当对象的生命周期结束时,它会被自动调用,用于释放对象可能持有的资源(如内存、文件句柄等)。
析构函数的名称与类名相同,但前面要加一个波浪符(~),并且没有返回类型,也没有参数。例如,如果有一个类名为
B,那么它的析构函数就会声明为 ~B()。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class B {
public:
// 构造函数
B() {
// 初始化代码,例如分配内存或打开文件等
}
// 析构函数
~B() {
// 清理代码,例如释放内存或关闭文件等
}
};
int main() {
B b; // 创建B类对象,构造函数被调用
// 当b离开作用域时,析构函数~B()会被自动调用
return 0;
}
2.3对象的复制,引用,指针创建
2.3.2浅拷贝与深拷贝
2.4const关键字与类
const是一种声明,对于const声明的变量/函数/对象等,我们不可以改变其数值(等),否则编译器会报错。
常量变量/指针
使用const声明的变量必须在声明时就赋值,且其值在程序运行期间不能再被修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const int a = 10; // a 是一个常量,其值不能被修改
// a = 20; // 错误!不能修改常量a的值
int x = 10;
int *const ptr = &x; // 常指针,指向x的地址,但ptr的值不能被改变
*ptr = 20; // 合法,因为可以修改指针所指向的内容
// ptr++; // 非法,因为ptr是常指针,其值不能改变
const int x = 10;
const int *ptr = &x; // 指向常量的指针,不能通过ptr修改x的值
// *ptr = 20; // 非法,因为ptr指向的内容是常量
int y = 20;
ptr = &y; // 合法,因为可以改变ptr的值
const int x = 10;
const int *const ptr = &x; // 指向常量的常指针
// *ptr = 20; // 非法,因为ptr指向的内容是常量
// ptr++; // 非法,因为ptr是常指针,其值不能改变
使用const修饰函数参数,可以保证在函数体内不会修改该参数的值。
1 |
|
简单地说,&是高效传参的手段,但是不具有const性质,我们选择在参数传递前面加上const,提高代码的可读性,又保证了const引用。
const成员函数
在成员函数中,const关键字表示该成员函数不会修改类的任何成员变量(除了被声明为mutable的成员)。这通常用于确保某些成员函数不会意外地修改对象的状态。
1 |
|
常对象(const Object)
常对象是用const关键字声明的对象。一旦一个对象被声明为const,就不能调用该对象的任何非const成员函数(因为这些函数可能会修改对象的状态),只能调用const成员函数。
1
2
3const MyClass obj(10); // 常对象
// obj.setValue(20); // 错误!不能调用非const成员函数来修改const对象
int val = obj.getValue(); // 正确!可以调用const成员函数
常对象成员(Members of a const Object)
当一个对象是const时,它的所有成员变量也都被视为const,即不能被修改。这意味着你不能在常对象内部修改任何成员变量的值。但是,你可以通过const成员函数来访问这些成员变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class MyClass {
public:
int x;
MyClass(int val) : x(val) {}
void showX() const { // 常成员函数可以访问常对象的成员
std::cout << x << std::endl;
}
// void modifyX(int newVal) const { x = newVal; } // 错误!不能在const成员函数中修改成员
};
int main() {
const MyClass myConstObj(5); // 常对象
myConstObj.showX(); // 可以调用const成员函数来访问成员
// myConstObj.x = 10; // 错误!不能修改const对象的成员
return 0;
}
1 |
|
注意:在 C++ 中,Point& const rb = b; 这样的写法实际上是不合法的。原因在于,引用本身在初始化后就不能改变其所绑定的对象,因此将引用本身声明为 const 是没有意义的。换句话说,引用本质上就是常量,一旦它绑定到了一个对象,就不能再被重新绑定到另一个对象。
所以,Point& const rb = b; 这样的声明是多余的,因为 rb 作为一个引用,本身就是不可变的。正确的声明应该只是 Point& rb = b;,表示 rb 是对 b 的一个引用。
2.5static 成员
1 |
|
这里的 calls 变量有以下几个特点:
生命周期:calls 的生命周期贯穿整个程序的执行期间,与全局变量的生命周期相同。这意味着,即使 function 函数返回后,calls 也不会被销毁,它的值会保留到下一次调用 function。
初始化:calls 只在程序第一次执行到该变量定义时被初始化一次,之后的函数调用中不会再次初始化。
作用域:与全局变量不同的是,calls 的作用域仅限于定义它的函数内部。这意味着,只有在 function 函数内部才能访问和修改 calls。在函数外部是无法直接访问这个变量的。
存储类别:static 局部变量存储在静态存储区,而不是栈内存。
因此,虽然 static int calls = 0; 在函数内部给 calls 赋予了类似全局变量的生命周期和存储类别,但它的作用域仍然是局部的,仅限于定义它的函数内。这与全局变量(在整个程序中都可见)是有所不同的。
注意:初始化静态成员变量必须在类外定义
2.6类的组合
在C++中,类的组合指的是在一个类中使用其他类的对象作为成员。这种技术允许我们构建更复杂的类,这些类由更简单的组件类组成。下面是一个简单的代码实例,展示了如何在C++中使用类的组合。
假设我们有一个Student类和一个Address类。每个学生都有一个地址,因此我们可以将Address类的对象作为Student类的一个成员。
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#include <iostream>
#include <string>
// Address类
class Address {
public:
Address(const std::string& street, const std::string& city, const std::string& country)
: street_(street), city_(city), country_(country) {}
void Print() const {
std::cout << "Street: " << street_ << ", City: " << city_ << ", Country: " << country_ << std::endl;
}
private:
std::string street_;
std::string city_;
std::string country_;
};
// Student类,它组合了一个Address对象
class Student {
public:
Student(const std::string& name, const Address& address)
: name_(name), address_(address) {}
void Introduce() const {
std::cout << "Hello, my name is " << name_ << ". I live at: ";
address_.Print();
}
private:
std::string name_;
Address address_; // 组合了一个Address对象
};
int main() {
// 创建一个Address对象
Address address("Main Street", "Smallville", "Country X");
// 创建一个Student对象,并传入Address对象作为组合成员
Student student("John Doe", address);
// 学生介绍自己,包括姓名和地址信息
student.Introduce();
return 0;
}
2.7友元
C++中的“友元”(friend)是一个特殊的声明,它允许一个或多个非成员函数或另一个类访问类的私有和保护成员。这是一种突破数据封装的方式,通常应当谨慎使用,但在某些情况下,它可以提供很大的便利,特别是在操作符重载和实现某些设计模式时。
注意事项
1 过度使用友元可能会破坏封装性,因此应谨慎使用。
2
友元关系不是对称的,也不是传递的。即,如果类A是类B的友元,这并不意味着类B也是类A的友元;同样,如果类A是类B的友元,类B是类C的友元,这并不意味着类A是类C的友元。
3
友元可以是全局函数、其他类的成员函数或整个类。在类定义中,使用friend关键字声明友元。
友元可以是一个函数,也可以是另一个类。下面,我将分别给出这两种情况的代码示例。
友元函数
1 |
|
当然,如果如果我把void printMyClass(const MyClass& obj) { std::cout << "Private variable is: " << obj.privateVar << std::endl; }
放进class的定义里面就不需要使用友元,因为成员函数可以直接访问类的所有成员,包括私有成员。
友元类
1 |
|
(三)多态与继承
3.1 运算符重载
重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。
3.1.1运算符重载
举个例子:复数的加法 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>
#include <string>
using namespace std;
class Cmycomplex {
private:
double real;
double imag;
public:
Cmycomplex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
//加法运算实现
Cmycomplex Add(const Cmycomplex& oz) {
Cmycomplex t;
t.real = real + oz.real;
t.imag = imag + oz.imag;
return t;
}
// 重载 + 运算符,作为成员函数
Cmycomplex operator+(const Cmycomplex& oz) {
return Add(oz);
}
};
int main() {
Cmycomplex z1(2, 3), z2, z3(3);
z2 = z1 + z3; // 使用重载的 + 运算符
z2.Show(); // 输出: (5+3i) 或者 (5+3i) 根据虚部的正负
return 0;
}1
2
3返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
这里是详细的过程:
1.当你写c1 + c2时,编译器会查找Cmycomplex类中是否定义了operator+成员函数。
2.编译器发现Cmycomplex类中确实定义了这样一个成员函数,于是它开始准备调用这个函数。
3.由于operator+是一个成员函数,它会自动地绑定到调用它的对象上,即c1。因此,c1本身就是函数的调用者(或称为接收者),不需要显式传递。
4.编译器将c2作为参数传递给c1.operator+(const Cmycomplex& oz)函数。在这个上下文中,oz就是c2的引用。
5.函数执行加法操作,并返回一个新的Cmycomplex对象,这个对象表示c1和c2的和。
6.所以,虽然看起来你只传递了一个参数(即c2),但实际上c1也参与了操作,因为它是调用operator+函数的对象。这就是为什么在成员函数内部可以通过this指针来访问调用对象的成员,因为成员函数隐式地与调用它的对象绑定。
简而言之,c1是通过成员函数调用的隐式绑定机制传入的,而c2是作为参数显式传入的。
那么如何重载”3+z1”这样的式子呢?
为了支持 3 + z1 这样的表达式,我们需要为 Cmycomplex 类提供一个非成员函数(友元函数)来重载 operator+,使其能够接受一个 double(或 int)作为第一个参数,和一个 Cmycomplex 对象作为第二个参数。
1 |
|
这在语法上是可行的,但是我们通常不把友元函数的功能在class内部实现,规范的代码应当将定义放在类外以保持代码的清晰和封装性。这样做也有助于将类的接口(即公共成员函数和变量)与其实现细节(即私有和保护成员)分离开来。
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#include <iostream>
#include <string>
//规范小驼峰命名从我做起
using namespace std;
class Cmycomplex {
private:
double real;
double imag;
public:
Cmycomplex(double r = 3.0, double i = 0.0) : real(r), imag(i) {}
//运算实现
Cmycomplex Add(const Cmycomplex& oz)
{
Cmycomplex t;
t.real=real+oz.real;
t.imag=imag+oz.imag;
return t;
}
//运算符重载
Cmycomplex operator+(const Cmycomplex& oz) {
return Add(oz);
}
//后缀求和
friend Cmycomplex operator+(double , const Cmycomplex &) ;
};
Cmycomplex operator+(double n, const Cmycomplex &oz) {
Cmycomplex t;
t.real=n+oz.real;
t.imag=oz.imag;
return t;
}
3.1.2流运算符重载
1 |
|
注意istream& operator>>(istream& in, Complex& c)无const,因为要改变c的内部状态了!!!
3.1.3自增和自减运算符重载
这个功能只能在类内实现。
1 |
|
3.2继承
保持已有类的特性而构造新类的过程称为继承(inheritance)。
在已有类的基础上新增自己的特性而产生新类的过程称为派生。
被继承的已有类称为基类(based class)(或父类)。
派生出的新类称为派生类(derived class)(或子类)
继承的目的:实现代码重用。
派生的目的:实现代码的可扩充性。当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
继承的真正魅力在于能够添加基类所没有的特点以及取代和改进从基类继承来的特点。
3.2.1子类父类
关于protected和private的区别:
简而言之,友元类既可以访问private也可以访问protected,但是派生类(子类)只能访问父类的protected
1 |
|
在这个示例中,我们定义了一个基类 Animal,它包含两个方法:eat() 和 sleep(),以及一个受保护的成员变量 name。然后,我们定义了一个派生类 Dog,它继承自 Animal 类。在 Dog 类中,我们添加了一个新的方法 bark()。
在 main() 函数中,我们创建了一个 Dog 类的对象 myDog,并调用了它的方法。注意,由于 Dog 继承自 Animal,所以 myDog 可以调用 Animal 类中定义的方法(eat() 和 sleep()),也可以调用 Dog 类中特有的方法(bark())。
关于public继承和private继承的区别
public继承
基类的public成员在派生类中仍然是public的。
基类的protected成员在派生类中仍然是protected的。
派生类外部的代码可以访问派生类中继承自基类的public和protected成员(如果派生类提供了访问这些成员的接口)。
private继承
基类的public和protected成员在派生类中都变成了private的。
派生类外部的代码无法直接访问派生类中继承自基类的任何成员。
在C++中,当派生类中存在与基类同名的成员变量或成员函数时,编译器需要一种机制来区分它们。这种情况通常发生在派生类重写(override)基类的成员函数,或者在派生类中声明了与基类同名的成员变量。
同名成员变量
对于成员变量,如果派生类中声明了与基类同名的成员变量,编译器会选择派生类中的变量。这意味着,如果你在派生类的方法中直接使用这个成员变量名,那么编译器将默认使用派生类中的变量,而不是基类中的。
如果你需要在派生类中访问基类中的同名成员变量,你可以使用作用域解析运算符(::)来明确指定基类的成员变量。例如:
同名成员函数(非虚函数)对于非虚成员函数,如果你在派生类中定义了一个与基类同名的成员函数(即没有使用 virtual 关键字),这个函数将隐藏基类中的同名函数,而不是重写它。这意味着,如果你通过派生类对象调用这个函数,将执行派生类中的版本。如果你有一个基类指针或引用指向派生类对象,并且调用了这个函数,那么将执行基类中的版本,因为这不是多态行为
3.2.2多继承
多继承是指一个类可以同时继承多个基类。
1 |
|
在这个例子中,我们有两个基类:Animal 和 Flyable。Animal 类有一个 eat 方法,而 Flyable 类有一个 fly 方法。然后,我们定义了一个派生类 Bird,它同时继承了 Animal 和 Flyable。Bird 类还定义了自己的 sing 方法。
在 main 函数中,我们创建了一个 Bird 对象,并调用了它的 eat、fly 和 sing 方法。由于 Bird 继承了 Animal 和 Flyable,因此它可以访问这两个基类的方法。
然而,多继承也可能导致一些问题,特别是菱形继承问题(也称为钻石继承或死亡钻石),这通常发生在当一个类从多个路径继承同一个基类时。这可能导致基类的多个实例在派生类中存在,从而引发歧义和浪费内存。这个问题可以通过虚继承(virtual inheritance)来解决,它确保在继承层次结构中只有一个共享的基类实例。
3.2.3继承时的构造函数
1 |
|
在上面的示例中,我们有一个基类Vehicle和一个派生类Car。Vehicle类有一个受保护的成员变量wheels和enginePower,以及一个公共的构造函数来初始化这些变量。Car类继承自Vehicle,并添加了一个新的私有成员变量doors。
在Car类的构造函数中,我们通过成员初始化列表来调用Vehicle的构造函数,以初始化从Vehicle继承的成员变量。这是通过: Vehicle(w, p)部分完成的,其中w和p是传递给Car构造函数的参数。然后,Car的构造函数继续初始化其自己的成员变量doors。
在main函数中,我们创建了一个Car对象,并传递了车轮数量、发动机功率和车门数量作为参数。这将依次调用Vehicle和Car的构造函数。最后,我们调用displayCarInfo函数来显示汽车的信息,该函数内部调用了基类的displayVehicleInfo函数来显示车轮数量和发动机功率的信息。
3.2.4虚函数
定义了一个基类Animal和两个派生类Dog和Cat。基类Animal中有一个虚函数makeSound(),它在派生类中被重写以提供不同的实现。在main()函数中,我们使用基类指针来调用不同派生类的makeSound()函数,展示了多态性的效果。
1 |
|
多态性就体现在指针类型是基类*指针,但是却可以指向他的所有派生类并调用所有的重写后的虚函数
如果一个类中的虚函数没有实现(即函数体为空),则这个函数被称为纯虚函数
1
2
3
4
5class AbstractBase {
public:
virtual void pureFunc() = 0; // 纯虚函数声明
};
3.2.5虚析构函数
对比几段代码
1 |
|
创建A*指针指向子类对象,一个指向派生类的指针可以隐式地转换为一个指向其基类的指针,这是类型安全的向上转型(upcasting)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#include <iostream>
using namespace std;
class A {
public:
~A() {
cout <<"destructor A\n";
}
};
class B : public A {
public:
~B() {
cout <<"destructor B\n";
}
};
int main() {
A* pa=new B();
delete pa;
return 0;
}
/*
destructor A
*/1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24#include <iostream>
using namespace std;
class A {
public:
virtual ~A() {
cout <<"destructor A\n";
}
};
class B : public A {
public:
~B() {
cout <<"destructor B\n";
}
};
int main() {
A* pa=new B();
delete pa;
return 0;
}
/*
destructor B
destructor A
*/
重要的是,基类 A 的析构函数被声明为
virtual。这是非常关键的,因为如果基类析构函数不是虚的,那么在删除派生类对象时就不会调用派生类的析构函数,编译器会根据删除指针的类型调用需要的函数。
3.2.6 虚继承
虚继承是面向对象编程中为了解决多重继承时的数据成员重复问题而引入的一种技术。 当一个指定的基类在继承体系中被多次继承时,虚继承可以确保这个基类的成员数据在派生类中只存在一份实例。
1 |
|
子类父类相互赋值
父类对象不能直接赋值给子类对象,因为子类可能包含父类没有的额外成员。但是,子类对象可以赋值给父类对象,即只保留子类对象中父类的部分。
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#include <iostream>
#include <string>
// 父类(基类)
class Animal {
public:
std::string name;
Animal(const std::string& n) : name(n) {}
virtual void speak() {
std::cout << "I'm an animal named " << name << std::endl;
}
};
// 子类(派生类)
class Dog : public Animal {
public:
std::string breed;
Dog(const std::string& n, const std::string& b) : Animal(n), breed(b) {}
void speak() override {
std::cout << "I'm a dog named " << name << " of breed " << breed << std::endl;
}
};
int main() {
// 创建一个Dog对象
Dog myDog("Rex", "Bulldog");
myDog.speak(); // 输出: I'm a dog named Rex of breed Bulldog
// 将Dog对象赋值给Animal对象(切片)
Animal myAnimal = myDog; // 切片发生在这里,只有Animal部分被复制
myAnimal.speak(); // 输出: I'm an animal named Rex
// 注意:以下代码是非法的,因为不能将父类对象赋值给子类对象
// Dog anotherDog = myAnimal; // 编译错误!
return 0;
}
1
2
3Animal* animalPtr = new Dog("Rex", "Bulldog");
animalPtr->speak(); // 如果speak是虚函数,则会调用Dog类的speak实现
与使用指针类似,我们也可以使用引用来实现多态性。引用允许我们直接通过基类引用来访问派生类对象,同时保持多态行为。
1
2
3Dog myDog("Rex", "Bulldog");
Animal& animalRef = myDog;
animalRef.speak(); // 如果speak是虚函数,则会调用Dog类的speak实现
3.3 理解“绑定”
在C++中,“绑定”通常指的是将函数调用与具体的函数实现关联起来的过程。根据绑定的时机,我们可以将其分为静态绑定(Static Binding)和动态绑定(Dynamic Binding)。
静态绑定(Static Binding)
静态绑定,也称为早期绑定(Early Binding),是指在编译时期就确定函数调用与具体实现的关联。在C++中,非虚函数的调用通常采用静态绑定。编译器在编译时会根据函数名和签名直接解析到具体的函数实现。
1 |
|
staticFunc 是一个非虚函数,因此它的调用在编译时期就已经确定。即使我们通过基类指针指向派生类对象,并尝试调用 staticFunc,也仍然会调用基类版本的函数。
动态绑定
动态绑定,也称为晚期绑定(Late Binding),是指在运行时才确定函数调用与具体实现的关联。在C++中,这通常通过虚函数来实现。当使用基类指针或引用调用虚函数时,实际调用的函数版本会根据指针或引用实际指向的对象类型动态确定。
1 |
|
dynamicFunc 是一个虚函数。当我们通过基类指针调用它时,实际调用的函数版本会根据指针指向的对象类型动态确定。这就是动态绑定的特点,它允许在运行时实现多态行为。
总结:
静态绑定在编译时期确定函数调用,而动态绑定在运行时确定。
静态绑定通常与非虚函数相关,而动态绑定则与虚函数相关。
动态绑定是实现多态性的关键机制之一。
4.c++模版
4.1函数模版
函数模版定义与调用
定义一个函数模板之后,没有指定类型,编译器会实现参数类型的自动推导。考虑单一类型T的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24#include <iostream>
using namespace std;
template <typename T> //开始泛型编程,由于历史原因,typename也可以写成class
T Max(const T& a,const T& b) {
return a > b ? a : b;
}
int main()
{
int n = 1;
int m = 2;
cout << "max(1, 2) = " << Max(n, m) << endl;
float a = 2.0;
float b = 3.0;
cout << "max(2.0, 3.0) = " << Max(a, b) << endl;
char i = 'a';
char j = 'b';
cout << "max('a', 'b') = " << Max(i, j) << endl;
return 0;
}template <typename T, typename T2>
注意T,T2必须在下文函数中使用,否则编译器会报错。尽管在下面的函数中,T,T2是允许被推导为同一个类型的。
1 |
|
函数模版与普通函数并存
当函数模版和普通函数并存的时候:
如果函数模板会产生更好的匹配,使用函数模板;
其他情况调用普通函数
1 |
|
对于这个函数模版
1 |
|
1 |
|
1 |
|
函数模版必须严格匹配,函数模板不允许自动类型转化!
嵌套使用
1 |
|
注意每一个函数模版都需要写自己的 template,如果你试图只写一个 template 声明,然后定义两个函数体,那么编译器将无法识别你的意图,并且会报错。每个模板函数都需要它自己的 template 声明。 ## 4.2类模版
模版类的定义
模板类(也称为类模板)提供了一种泛型编程的机制,它允许用户为类定义一个蓝图,这个蓝图可以适应不同的数据类型或参数。
模版类的含义就是提供一组可以泛型编程的变量给下面定义的类使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template<typename T>
class Array {
private:
T* data;
int size;
public:
Array(int s) : size(s) {
data = new T[size];
}
~Array() {
delete[] data;
}
T& operator[](int index) {
return data[index];
}
int getSize() const {
return size;
}
};1
2
3Array<int> intArray(10); // 创建一个存储整数的数组
Array<double> doubleArray(5); // 创建一个存储双精度浮点数的数组
含有常量的实例化
1 |
|
模版类的偏特化与全特化
所谓特化。就是说我们有时候并不想泛化所有模版,我们希望有些类型的模版实现特殊的泛化,为此:
全特化(Full Specialization)
全特化是指针对模板类的所有模板参数都提供具体的类型。下面是一个模板类及其全特化的示例:
1 |
|
偏特化(Partial Specialization)
偏特化是指只针对模板类的部分模板参数提供具体的类型。这通常用于模板类有多个类型参数的情况。下面是一个模板类及其偏特化的示例:
1 |
|
5.异常处理
给出一段三角形相关的异常处理 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#include <iostream>
#include <cmath>
#include <string>
using namespace std;
double triangle(double a,double b,double c)//计算三角形面积的函数
{
if(a+b<=c||a+c<=b||b+c<=a) //无法构成三角形则丢出异常
{
string s="that is not a triangle!";
throw s;
}
double s=(a+b+c)/2;
return sqrt(s*(s-a)*(s-b)*(s-c));
}
int main()
{
double triangle(double,double,double);
double a,b,c;
cin>>a>>b>>c;
if(a>0&&b>0&&c>0)
{
try{
cout<<triangle(a,b,c)<<endl;
}
catch(string& s){
cout<<"a="<<a<<",b="<<b<<",c="<<c<<","<<s;
}
cin>>a>>b>>c;
}
}
实现的核心就在于前面throw出异常,后面try并且catch的过程。
1 |
|
需要注意的是,这个写法是简化的,并不标准(可以正常运行),规范的代码throw后面有给定的异常类。
给出一个除以0的例子
1 |
|
以下是一些C++标准库中常见的异常类:
std::logic_error:这是所有逻辑错误的基类。逻辑错误通常指的是程序员的错误,例如无效的参数。
std::logic_error的直接派生类包括:
std::invalid_argument:表示函数接收到了无效的参数。
std::domain_error:表示参数的值域不正确,即参数的值不在预期的范围内。
std::length_error:试图创建一个超过其最大尺寸的容器时抛出。
std::out_of_range:试图访问容器的一个不存在的元素时抛出。
std::runtime_error:这是所有运行时错误的基类。运行时错误指的是那些在运行时才能检测到的错误,如资源不足。
std::runtime_error的直接派生类包括:
std::range_error:表示一个值不在其预期的范围之内时抛出。
std::overflow_error:表示算术运算的结果太大,无法用给定的类型表示时抛出。
std::underflow_error:表示算术运算的结果太小,无法用给定的类型表示时抛出。
std::bad_alloc:当内存分配失败时,如使用new关键字时内存不足,会抛出此异常。
std::bad_cast:当进行不安全的类型转换,如dynamic_cast到一个不合适的类型时,会抛出此异常。
std::bad_typeid:当typeid操作符被用于一个空指针时,会抛出此异常。
std::ios_base::failure:输入输出流错误时会抛出此异常,它是std::exception的派生类,但通常与IO操作关联。
除了上述的标准异常外,C++还允许程序员定义自己的异常类。当你需要表示特定的错误条件时,可以创建从std::exception派生的新类。
6.c++文件操作
写入文件
1 |
|
读取文件
1 |
|