C++基础(二)
·
1. 重载
1.1 函数重载
• 在同一个作用域内,可以声明几个功能类似的同名函数。
• 这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。
• 下面的实例中,同名函数 print() 被用于输出不同的数据类型:
#include <iostream>
#include <string>
using namespace std;
class printData {
public:
void print(int data);
void print(double data);
void print(string data);
};
void printData :: print(int data){
cout << "输出的是整型数:" << data << endl;
}
void printData :: print(double data){
cout << "输出的是浮点数:" << data << endl;
}
void printData :: print(string data){
cout << "输出的是字符串:" << data << endl;
}
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
printData *p = new printData();
p->print(1);
p->print(1.1);
p->print("Hello World!");
return 0;
}
1.2 运算符重载
• 在C++中,运算符重载是一个允许自定义各种运算符(如 + , - , == , != 等)在自定义类型(类或结构体)上的行为特性。这意味着你可以定义类似于内置类型的运算符行为,使你的自定义类型更加
直观和易于使用。
• 基本原则
1. 不可以创建新的运算符:只能重载已经存在的运算符。
2. 至少有一个操作数是用户定义的类型:不能重载两个基本类型的运算符。
3. 不能更改运算符的优先级:重载的运算符保持其原有的优先级和结合性。
• 示例1:假设我们有一个Person 类,我们可以重载 == 运算符来实现两个Person是否相等的判断。
#include <iostream>
#include <string>
using namespace std;
class Person{
public:
string name;
int id;
bool operator==(Person tmp);
};
bool Person::operator==(Person tmp){
return tmp.name == name && tmp.id == id;//此时,重载之后,这个类才有==这样的比较。
}
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
//创建两个人比较
// Person p1;
// p1.name = "jack";
// p1.id = 1;
// Person p2;
// p2.name = "tom";
// p2.id = 2;
Person p1;
p1.name = "jack";
p1.id = 1;
Person p2;
p2.name = "jack";
p2.id = 1;
// bool ret = p1 == p2,直接将两个对象对比p1 == p2,
//会报错,因为Person类没有定义 == 运算符
//所以需要进行运算符重载。
bool ret = p1 == p2;
cout << "比较的结果是:" << ret << endl;
return 0;
}
• 示例2:假设我们有一个简单的 Point 类,我们可以重载 + 运算符来实现两个点的加法。
#include <iostream>
using namespace std;
class Point{
public:
int x,y;
Point operator+(Point tmp){
Point ret;
ret.x = tmp.x + x;
ret.y = tmp.y + y;
return ret;
}
};
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
Point p1;
p1.x = 1;
p1.y = 2;
Point p2;
p2.x = 2;
p2.y = 3;
//不用重载的情况
// Point p3;
// p3.x = p2.x + p1.x;
// p3.y = p2.y + p1.y;
Point p3 = p1 + p2;
cout << "p3的x为" << p3.x << ",p3的y为:" << p3.y << endl;
return 0;
}
2. 构造函数
2.1 什么是构造函数
• 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
• 那构造函数的作用是初始化成员变量,加载内存空间等
• 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
2.2 构造函数的分类
• 默认的构造函数没有任何参数(无参构造函数),但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
2.3 初始化列表
• 在C++中,使用初始化列表来初始化类的字段是一种高效的初始化方式,尤其在构造函数中。初始化列表直接在对象的构造过程中初始化成员变量,而不是先创建成员变量后再赋值。
• 初始化列表紧跟在构造函数参数列表后面,以冒号( : )开始,后跟一个或多个初始化表达式,每个表达式通常用逗号分隔。下面是使用初始化列表初始化字段的例子:
#include <iostream>
#include <string>
using namespace std;
class MyClass{
private:
int x;
string y;
public:
MyClass(int id,string name) : x(id),y(name){
}
void printinfo(){
cout << "id为:" << x << "名字为:" << y;
}
};
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
MyClass(1,"tom").printinfo();
return 0;
}
• 在这个例子中, MyClass 有两个成员变量:x(int 类型), y(string类型)。当创建 MyClass 的一个实例时,我们通过构造函数传递两个参数,这些参数被用于通过初始化列表直接初始化成员变量。初始化列表 : x(id), y(name)的意思是用 id初始化x,用name初始化 y 。
• 初始化列表的优点包括:
1. 效率:对于非基本类型的对象,使用初始化列表比在构造函数体内赋值更高效,因为它避免了先默认构造然后再赋值的额外开销。
2. 必要性:对于引用类型和常量类型的成员变量,必须使用初始化列表,因为这些类型的成员变量在构造函数体内不能被赋值。
3. 顺序:成员变量的初始化顺序是按照它们在类中声明的顺序,而不是初始化列表中的顺序。
2.4 this关键字
• this是一个特殊的指针,指向当前对象的地址。作用域是类的非静态成员函数内部。
2.5 new关键字
• 在C++中, new关键字用于动态分配内存。它是C++中处理动态内存分配的主要工具之一,允许在程序运行时根据需要分配内存。
• 与delete配对使用:使用new分配的内存必须显式地通过delete(对于单个对象)或delete[](对于数组)来释放,以避免内存泄露。
• 注意事项
• 异常安全:如果new分配内存失败,它会抛出std::bad_alloc异常(除非使用了 nothrow 版本)。
• 内存泄露:忘记释放使用new分配的内存会导致内存泄露。
• 匹配使用 delete 和 delete[]:为避免未定义行为,使用new分配的单个对象应该使用delete释放,使用new[]分配的数组应该使用delete[]释放。
3. 析构函数
3.1 什么是析构函数
• 析构函数是C++中的一个特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行对象销毁前的清理工作。析构函数特别重要,尤其是在涉及动态分配的资源(如内存、文件句柄、网络连接等)的情况下。
• 基本特性
1. 名称:析构函数的名称由波浪号( ~ )后跟类名构成,如 ~MyClass() 。
2. 无返回值和参数:析构函数不接受任何参数,也不返回任何值。
3. 自动调用:当对象的生命周期结束时(例如,一个局部对象的作用域结束,或者使用 delete 删除一个动态分配的对象),析构函数会被自动调用。
4. 不可重载:每个类只能有一个析构函数。
5. 继承和多态:如果一个类是多态基类,其析构函数应该是虚的。
• 示例
#include <iostream>
#include <cstdlib>
using namespace std;
class MyClass{
private:
int* datas;
public:
MyClass(int size){
datas = new int[size];
}
~MyClass(){
cout << "析构函数被调用" << endl;
delete[] datas;
}
};
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
MyClass m1(5);
MyClass *m2 = new MyClass(10);
delete m2;
_sleep(5);
cout << "hello world" << endl;
_sleep(5);
return 0;
}
//执行结果是
//析构函数被调用
//hello world
//析构函数被调用
• 执行过程:
• 第一次执行"析构函数被调用" 这是因为在堆上面分配完内存之后,执行delete这个操作,触发了析构函数的执行。
• 第二次执行"析构函数被调用" 这是因为m1在离开main栈作用域后,自动触发了析构函数的执行。
3.2 析构函数的要点
| 要点 | 描述 |
|
定义和作用
|
析构函数在对象生命周期结束时自动调用,用于清理对象可能持有的资源。
|
| 语法 |
析构函数名称由波浪线 (~) 后跟类名构成,例如 MyClass 的析构函数为~MyClass() 。
|
| 资源管理 |
用于释放对象在生命周期中分配的资源,如动态内存、文件句柄、网络连接等。
|
| 自动调用机制 |
当对象离开其作用域或通过 delete 删除时,将自动调用其析构函数。
|
| 防止资源泄露 |
正确实现析构函数对防止资源泄露至关重要,特别是在涉及动态资源分配的情况。
|
| 序析构函数 |
如果类作为基类设计,应有一个虚析构函数,以确保正确调用派生类的析构函数。
|
| 析构函数与异常 |
析构函数不应抛出异常,如果可能抛出,应在函数内捕获。
|
| 删除的析构函数 |
可以通过将析构函数声明为删除( ~MyClass() = delete; )来禁止删除某类对象。
|
| 与析构函数的关系 |
每个类只能有一个析构函数,不可重载,与构造函数相比。
|
4. 静态成员
4.1 静态成员的定义
• 静态成员在C++类中是一个重要的概念,它包括静态成员变量和静态成员函数。静态成员的特点和存在的意义如下:
• 静态成员变量
1. 定义:静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建了多少个类的实例,静态成员变量只有一份拷贝(静态成员只加载一次)。
2. 初始化:静态成员变量需要在类外进行初始化,通常在类的实现文件中。
3. 访问:静态成员变量可以通过类名直接访问,不需要创建类的对象。也可以通过类的对象访问。
4. 用途:常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共享。
• 静态成员函数
1. 定义:静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和非静态成员函数。
2. 访问:类似于静态成员变量,静态成员函数可以通过类名直接调用,也可以通过类的实例调用。
3. 用途:常用于实现与具体对象无关的功能,或访问静态成员变量。
• 存在的意义
• 共享数据:允许对象之间共享数据,而不需要每个对象都有一份拷贝。
• 节省内存:对于频繁使用的类,使用静态成员可以节省内存。
• 独立于对象的功能:静态成员函数提供了一种在不创建对象的情况下执行操作的方法,这对于实现工具函数或管理类级别状态很有用。
• 示例:
#include <iostream>
using namespace std;
class MyClass{
public:
static int value;
MyClass(){
this->value++;
}
~MyClass(){
this->value--;
}
static int getStaticInstance(){
return value;
}
};
//静态成员变量需要在类外进行初始化,通常在类的实现文件中
int MyClass :: value = 0;
int main()
{
system("chcp 65001 > nul");//强制控制台用UTF-8编码
MyClass p1,p2;
//cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
MyClass *p3 = new MyClass();
//cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
delete p3;
//cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
return 0;
}
5. 继承
5.1 基础的基本概念
• 继承是面向对象编程(OOP)中的一个核心概念,特别是在C++中。它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码重用,以及建立一种类型之间的层次关系。
• 特点
1. 代码重用:子类继承了父类的属性和方法,减少了代码的重复编写。
2. 扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
3. 多态性:通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数。
• 基本用法
•在C++中,继承可以是公有(public)、保护(protected)或私有(private)的,这决定了基类成员在派生类中的访问权限。
5.2 权限对继承的影响
• 在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承(public 、 protected 、 private)如何影响基类的不同类型成员(public 、 protected 、 private)在派生类中的访问级别。
| 基类成员类型 | public继承 | protected继承 | private继承 |
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
• 解释:
• public继承:基类的public成员在派生类中仍然是public的, protected 成员仍然是protected的。基类的private成员在派生类中不可访问。
• protected继承:基类的public和protected成员在派生类中都变成protected的。基类的private成员在派生类中不可访问。
• private继承:基类的public和protected成员在派生类中都变成 private的。基类的private成员在派生类中不可访问。
• 基类的private成员始终不可直接在派生类中访问!!!!!
• 访问权限
| 访问权限 | 类内部 | 同一个类的对象 | 子类 | 类外部 |
| public |
✔️ 可访问
|
✔️ 可访问
|
✔️ 可访问
|
✔️ 可访问
|
| protected |
✔️ 可访问
|
❌ 不可访问
|
✔️ 可访问 |
❌ 不可访问
|
| private |
✔️ 可访问
|
❌ 不可访问
|
❌ 不可访问
|
❌ 不可访问
|
• 示例
#include <iostream>
using namespace std;
//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public://公有数据
string type;
string contry;
string color;
double price;
int numOfWheel;
protected://受保护数据
int protectedData;
private://私有数据
int privateData;
public:
void run(){
cout << "车跑起来了" << endl;
}
void stop();
};
//私有继承测试
//私有继承了父类,所以public和protected修饰的成员都变成private
//而原本private修饰的成员是不可访问的。
class TestClass : private Vehicle{
public:void tsetFunc(){
//基类的公有数据被私有继承后,在子类中权限变成私有,只限在本类使用,
//此时这个price是私有的。
price = 10;
}
};
//保护继承测试
//受保护继承父类,所以public和protected修饰的成员变成protected修饰符修饰的成员
//而原本private修饰的成员是不可访问的。
class Truck : protected Vehicle{
public:
void testFunc(){
type = "数据测试"; //变成protected权限。
protectedData = 10; //还是protected受保护权限
privateData = 10; //报错了,基类的私有成员,不管哪种方式的继承都是不可访问的。
}
};
//公有继承测试
//公有继承父类,所以public和protected修饰的成员都还是原来的修饰符
//而原本private修饰的成员是不可访问的。
class Bickle : public Vehicle{
public:
void testFunc(){
protectedData = 10;
}
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:
int stateOfTop;
void openTopped();
void pdrifting();
};
int main()
{
TestClass test;
test.price = 3.3; //报错了,基类的公有成员被私有继承后,降为私有权限
Truck t;
t.type = "测试"; //报错了,基类的公有成员被保护继承后,降为保护权限,因为protected修饰的成员不能在类外部使用
t.protectedData = 10; //从报错信息看出,保护继承造成基类的保护成员还是保持保护权限
Roadster ftype;
ftype.type = "捷豹Ftype";
ftype.run();
Bickle bike;
bike.type = "死飞";
bike.run();
return 0;
}
5.3 基类构造函数
• 在C++中,子类可以通过其构造函数的初始化列表来调用基类的构造函数。这是在构造子类对象时初始化基类部分的标准做法。 当创建子类的对象时,基类的构造函数总是在子类的构造函数之前被调用。如果没有明确指定,将调用基类的默认构造函数(无参构造方法)。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,就需要在子类构造函数的初始化列表中明确指定。
5.4 虚函数
• 在C++中, virtual和override关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。
• virtual 关键字
1. 使用场景:在基类中声明虚函数。
2. 目的:允许子类重写该函数,实现多态。
3. 行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本。
• override 关键字
1. 使用场景:在派生类中重写虚函数。
2. 目的:明确指示函数意图重写基类的虚函数。
3. 行为:确保子类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错
• 注意点
• 只在子类中使用override:override应仅用于子类中重写基类的虚函数。
• 虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。
• 默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual
关键字才会成为虚函数。
• 继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有子类中自动成为虚函数,无论是否使用 virtual 关键字。
5.5 多重继承
• 在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着子类可以继承多个基类的属性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同的成员时。
• 基本概念
• 在多重继承中,子类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
• 注意事项
• 菱形继承问题:如果两个基类继承来自同一个更高层的基类,这可能导致子类中存在两份基类的副本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
• 复杂性:多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时。
• 设计考虑:虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些情况下,使用组合或接口(纯虚类)可能是更好的设计选择。
5.6 虚继承
• 虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一个类继承自两个具有共同基类的类时,会导致共同基类的成员在子类中存在两份拷贝,这不仅会导致资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来解决这一问题。
• 菱形继承的示例
#include <iostream>
using namespace std;
class Base {
public:
int data;
};
class Derived1 : public Base {
// 继承自 Base
};
class Derived2 : public Base {
// 继承自 Base
};
class FinalDerived : public Derived1, public Derived2 {
// 继承自 Derived1 和 Derived2
};
int main()
{
FinalDerived f;
// f.data;这样直接调用报错
return 0;
}
• 在这个例子中, FinalDerived 类通过Derived1和Derived2间接地继承自 Base 类两次。因此,
它包含了两份 Base的成员拷贝(有两份data)。
• 使用虚继承解决菱形继承问题
#include <iostream>
using namespace std;
class Base {
public:
int data;
};
class Derived1 : virtual public Base {
// 继承自 Base
};
class Derived2 : virtual public Base {
// 继承自 Base
};
class FinalDerived : public Derived1, public Derived2 {
// 继承自 Derived1 和 Derived2
};
int main()
{
FinalDerived f;
f.data;//这样直接调用报错,但是使用了虚继承之后就可以进行编译了
return 0;
}
• 通过将Derived1和Derived2对Base的继承声明为虚继承(virtual public Base)之后,FinalDerived 类中只会有一份Base类的成员。无论通过Derived1还是Derived2的路径,访问的都是同一个 Base类的成员。
• 特点和注意事项
• 初始化虚基类:在使用虚继承时,虚基类(如上例中的Base类)只能由最派生的类(如FinalDerived)初始化。
• 内存布局:虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
• 设计考虑:虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如
组合或接口)避免菱形继承,那通常是更好的选择。
更多推荐


所有评论(0)