本文章为学习C++时的笔记,没有严格按照参考书顺序学习:
-
2 初识C++
-
2.1面向对象程序思想
关键:如何利用面向对象思想解决问题
三大特征:1封装 2继承 3多态
-
2.2.1类的定义
一种用户自定义的数据类型。用于描述一组对象的共同属性和行为。
class 类名 { 权限控制符: 变量和函数声明; }
类名是一种标识符,命名规则通常首字母大写
大括号中可以定义变量/函数:
定义的变量成为成员变量(属性);定义的函数成为成员函数(方法)。成员变量用于描述对象的属性,成员函数用于描述对象的行为。
成员变量和成员函数统称为类的成员。
类的所有成员都要在这一对大括号中声明。
class Student//定义类 { public: void study(); void exam(); }
-
2.2.2类的实现
在源文件中对类进行实现。
类的成员函数的实现通常需要添加作用域限定符“类名::”加以限定。
void Student::study() { } void Student::exam() { }
在类定义中,可以使用访问权限控制符对类的成员进行访问控制,从而实现封装。C++中有三种访问权限控制符:public、private和protected。
-
void Student::study() { } void Student::exam() { }
在类定义中,可以使用访问权限控制符对类的成员进行访问控制,从而实现封装。C++中有三种访问权限控制符:public、private和protected。
- public:公有权限,表示该成员可以被任何函数访问。
- private:私有权限,表示该成员只能被类的成员函数访问,其他函数不能访问。
- protected:保护权限,表示该成员可以被派生类的成员函数访问,其他函数不能访问。
class MyClass { public: int publicMember; //公有成员 protected: int protectedMember; //保护成员 private: int privateMember; //私有成员 };
在类的外部访问类的成员时,需要使用成员访问运算符”.”或”->”。其中,点运算符”.”用于访问对象的成员,箭头运算符”->”用于访问指向对象的指针的成员。
MyClass obj; //定义MyClass类的对象 obj.publicMember = 1; //访问公有成员 //obj.protectedMember = 2; //错误,访问保护成员 //obj.privateMember = 3; //错误,访问私有成员 MyClass *ptr = new MyClass(); //定义指向MyClass类对象的指针 ptr->publicMember = 1; //访问公有成员 //ptr->protectedMember = 2; //错误,访问保护成员 //ptr->privateMember = 3; //错误,访问私有成员
eg:定义一个手机类,手机有颜色,型号等属性;有打电话,发短信等功能。
class Phone { private: string color; string model; public: void call(); void sendMessage(); }; void Phone::call() { } void Phone::sendMessage() { }
-
2.2.3对象的创建与使用
在类定义中,我们只是定义了一个模板,为了使用类,我们需要创建一个对象。对象是类的实例,可以使用类定义的成员属性和成员函数。可以使用类名或者类的对象来访问类的成员。
class MyClass { public: int publicMember; //公有成员 void publicFunction(); //公有成员函数 }; MyClass obj; //定义MyClass类的对象 MyClass *ptr = new MyClass(); //定义指向MyClass类对象的指针 obj.publicMember = 1; //访问公有成员 ptr->publicFunction(); //访问公有成员函数
在创建对象时,系统会自动调用构造函数进行初始化,释放对象时,系统会自动调用析构函数进行清理。构造函数和析构函数是类的特殊成员函数,用于初始化和清理对象。构造函数和析构函数的函数名与类名相同,构造函数没有返回值,析构函数不需要参数。
class MyClass { public: int publicMember; //公有成员 MyClass(); //构造函数 ~MyClass(); //析构函数 }; MyClass::MyClass() { //初始化... } MyClass::~MyClass() { //清理... }
我们可以在构造函数中进行对象的初始化工作,例如分配内存、设置默认值等,而在析构函数中进行对象的清理工作,例如释放内存、关闭文件等。当对象被销毁时,系统会自动调用析构函数。
class MyClass { public: int *data; //动态分配的内存 MyClass(); ~MyClass(); }; MyClass::MyClass() { data = new int[100]; //分配100个int类型的内存 } MyClass::~MyClass() { delete[] data; //释放内存 }
在类的成员函数中,可以使用this指针访问当前对象的成员。this指针是一个隐含的指针,指向当前对象本身。可以使用this->成员名的方式访问成员。this指针在成员函数被调用时自动传入,不需要显式传递。
class MyClass { public: int publicMember; void print() { cout << this->publicMember << endl; //使用this指针访问成员 } }; MyClass obj; obj.publicMember = 1; obj.print(); //输出1
完整的C++类与对象代码
#include #include using namespace std; class Person { public: string name; //姓名 int age; //年龄 string gender; //性别 Person(); //构造函数 ~Person(); //析构函数 void sayHello(); //成员函数 }; Person::Person() { cout << "Person对象被创建了" << endl; } Person::~Person() { cout << "Person对象被销毁了" << endl; } void Person::sayHello() { cout << "大家好,我叫" << name << ",今年" << age << "岁,是一位" << gender << "士。" << endl; } int main() { Person p; //创建Person对象 p.name = "张三"; p.age = 18; p.gender = "男"; p.sayHello(); //调用成员函数 return 0; }
-
2.3封装
- 封装是面向对象编程的一种基本特征,用于隐藏对象的实现细节,只暴露必要的接口,以提高程序的安全性、可靠性和可维护性。封装可以通过访问权限控制符来实现,将对象的成员分为公有、私有和保护三种,使得不同的成员只能被不同范围内的函数访问。C++中的封装实现通过访问权限控制符来实现,具体如下:
- public:公有权限,表示该成员可以被任何函数访问。
- private:私有权限,表示该成员只能被类的成员函数访问,其他函数不能访问。
- protected:保护权限,表示该成员可以被派生类的成员函数访问,其他函数不能访问。下面是一个例子,定义一个人类,有姓名和年龄两个私有属性,提供设置姓名和年龄的公有方法和获取姓名和年龄的公有方法:
class Person { private: string name; int age; public: void setName(string name) { this->name = name; } void setAge(int age) { this->age = age; } string getName() { return this->name; } int getAge() { return this->age; } };
在上面的例子中,姓名和年龄被定义为私有成员变量,只能在类的内部访问,外部无法直接访问。通过公有的setter和getter方法来访问私有成员变量,这样就可以对成员变量进行封装。
-
2.4 this指针
- this指针是C++中一个隐式的指针,指向当前对象本身。在类的成员函数中,可以使用this指针访问当前对象的成员,从而避免了成员变量和函数参数之间的命名冲突。使用this指针的语法为:this->成员名
下面是一个例子,定义一个人类,有姓名和年龄两个私有属性,提供设置姓名和年龄的公有方法和获取姓名和年龄的公有方法,使用this指针来访问成员变量:
class Person { private: string name; int age; public: void setName(string name) { this->name = name; } void setAge(int age) { this->age = age; } string getName() { return this->name; } int getAge() { return this->age; } };
在上面的例子中,this指针被用于访问成员变量name和age,避免了和函数参数重名的问题。
-
2.5 构造函数
构造函数是一种特殊的成员函数,它在创建对象时被调用,用于初始化对象的成员变量。构造函数的名称与类名相同,没有返回类型,可以重载,并且可以包含参数列表。
以下是构造函数的一些特点:
- 构造函数在创建对象时自动调用,不需要手动调用。
- 构造函数在对象被创建时只会执行一次,它主要用于初始化对象的成员变量,确保对象在创建时具有合适的初始状态。
- 构造函数可以具有不同的参数列表,以便创建对象时提供不同的初始化方式。这被称为构造函数的重载。
- 构造函数可以被定义为公有、私有或保护。如果构造函数被定义为私有,则只有该类的成员函数可以调用它。
- 如果没有显式定义构造函数,则编译器会提供默认构造函数,它将所有成员变量设置为默认值。
- 构造函数可以使用成员初始化列表来初始化成员变量。成员初始化列表出现在构造函数体之前,使用冒号分隔,提供了一种更有效的初始化方法。
- 构造函数可以抛出异常,如果在对象创建期间发生错误,则可以通过异常处理机制进行处理。
总之,构造函数是一种重要的类成员,它可以确保对象在创建时具有正确的初始状态,同时也为我们提供了一个自定义对象初始化方式的机制。
下面是一个简单的例子,演示如何定义和使用构造函数来初始化对象的成员变量:
#include using namespace std; class Rectangle { private: int width; int height; public: Rectangle() { width = 0; height = 0; } Rectangle(int w, int h) { width = w; height = h; } int getArea() { return width * height; } }; int main() { Rectangle rect1; // 使用默认构造函数 Rectangle rect2(5, 10); // 使用带参数的构造函数 cout << "Rect1 area: " << rect1.getArea() << endl; cout << "Rect2 area: " << rect2.getArea() << endl; return 0; }
在上面的示例中,我们定义了一个名为
Rectangle
的类,并声明了两个构造函数。第一个构造函数是默认构造函数,不带参数,将宽度和高度设置为0。第二个构造函数带有两个参数,用于初始化矩形的宽度和高度。在主函数中,我们使用默认构造函数创建一个名为
rect1
的矩形对象,使用带参数的构造函数创建一个名为rect2
的矩形对象。然后,我们使用getArea
函数计算并打印出这两个矩形的面积。注意,在构造函数中,我们使用了赋值语句来初始化成员变量。这里也可以使用成员初始化列表,例如:
cppCopy code Rectangle(int w, int h) : width(w), height(h) { }
使用成员初始化列表可以提供更高效的初始化方法,因为它可以直接在对象创建时初始化成员变量,而不是在构造函数体中执行赋值操作。但是,在这个简单的例子中,我们使用了赋值语句来更清楚地表达代码的意图。
-
2.5.3含有成员对象的类的的构造函数
- 在建立该类的对象时,先调用成员对象的构造函数,然后才执行该类自己的构造函数。
- 在销毁该类的对象时,先调用该类自己的析构函数,然后才调用成员对象的析构函数。
- 在定义该类的构造函数时,可以在初始化列表中显式地指定要调用哪个成员对象的构造函数,并传递相应的参数。如果没有指定,则默认调用成员对象无参或默认参数的构造函数。
- 如果要调用成员对象拷贝或移动构造函数,则需要在初始化列表中使用同名参数或右值引用作为源对象。
- 在该类自己的构造函数体内,不要再对成员对象进行赋值操作,因为这会导致多余地调用赋值运算符重载函数。假设有一个Birth类,表示出生日期,有三个成员变量year、month和day,以及一个构造函数和一个显示函数。还有一个Student类,表示学生信息,有四个成员变量name、id、score和birth,其中birth是Birth类的对象。Student类也有一个构造函数和一个显示函数。
示例代码如下:
//定义Birth类 class Birth { public: //构造函数 Birth(int y = 0, int m = 0, int d = 0) { year = y; month = m; day = d; } //显示函数 void show() { cout << year << "-" << month << "-" << day << endl; } private: int year; //年份 int month; //月份 int day; //日期 }; //定义Student类 class Student { public: //构造函数,在初始化列表中调用Birth类的构造函数 Student(string n, string i, double s, int y, int m, int d) : name(n), id(i), score(s), birth(y,m,d) {} //显示函数 void show() { cout << "Name: " << name << endl; cout << "ID: " << id << endl; cout << "Score: " << score << endl; cout << "Birth: "; birth.show(); //调用Birth类的显示函数 } private: string name; //姓名 string id; //学号 double score; //成绩 Birth birth; //出生日期,是Birth类的对象 }; //主函数,创建并显示两个Student对象 int main() { Student s1("Alice", "2021001", 95.5, 2001, 1, 1); Student s2("Bob", "2021002", 88.8, 2002, 2 ,2); s1.show(); s2.show(); return 0; }
-
2.6析构函数
创建对象时,系统会为对象分配所需要的内存空间
析构函数有编译器自动定义
注意:
- 析构函数是一个成员函数,它的名称是类名的前面加一个“~”符号,例如 ~String() 。
- 析构函数不能带有任何参数,也不能返回任何值,因此不能重载。
- 析构函数在对象被销毁时自动调用,例如对象超出范围或通过delete显式销毁。
- 析构函数可以访问类的成员数据和成员函数,可以用来释放对象占用的资源,例如内存空间。
- 如果没有定义析构函数,编译器会自动生成一个默认的析构函数,但它可能不会释放对象分配的资源。
- 可以在类的声明中定义析构函数,也可以在类的外部定义析构函数,例如:
//在类的声明中定义析构函数 class String { public: String(); //构造函数 ~String(); //析构函数 private: char* data; //字符串数据 }; //在类的外部定义析构函数 String::~String() { delete[] data; //释放字符串数据占用的内存空间 }
-
2.7拷贝构造函数
-
2.7.1定义
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
拷贝构造函数在以下三种情况下会被调用:
- 当用一个对象去初始化同类的另一个对象时。
- 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用。
- 当函数的返回值是类的对象或引用时。拷贝构造函数的定义形式如下:
class_name(const class_name &obj) { // 构造函数体 }
拷贝构造函数的调用有以下三种方式:
- 直接初始化:
class_name obj1(obj2);
或者class_name obj1 = obj2;
- 作为函数参数传递:
void func(class_name obj); func(obj1);
- 作为函数返回值:
class_name func(); class_name obj1 = func();
-
2.7.2浅拷贝
浅拷贝是指拷贝时只拷贝对象本身,而不拷贝对象包含的引用所指向的对象。
拷贝出来的对象的所有变量的值都含有与原来对象相同的值,而所有对其他对象的引用都指向原来的对象。
简单地说,浅拷贝只拷贝对象不拷贝引用。
浅拷贝的缺陷是当你更改源或副本时,也可能导致其他对象也发生更改
#include using namespace std; class Sheep//定义绵羊类Sheep { public: Sheep(string name, string color);//带参数构造函数 Sheep(const Sheep& another);//拷贝构造函数 void show();//普通成员函数 ~Sheep();//析构函数 private: string _name;//成员:姓名 string _color;//成员:颜色 }; Sheep::Sheep(string name, string color) { cout << "调用构造函数" << endl; _name = name; _color = color; } Sheep::Sheep(const Sheep& another) {//类外实现拷贝构造函数 cout << "调用拷贝构造函数" << endl; _name = another._name; //将形参another的成员变量赋给类的成员变量 _color = another._color; } void Sheep::show() { cout << _name << " " << _color << endl; } Sheep::~Sheep() { cout << "调用析构函数" << endl; } int main() { Sheep sheepA("Doly", "white"); cout << "sheepA:"; sheepA.show(); Sheep sheepB(sheepA);//使用sheepA初始化新对象sheepB cout << "sheepB:"; sheepB.show(); return 0; }
-
2.8修饰类成员
-
2.8.1 const(动态成员变量)
-
2.8.2 static(静态成员变量)
static修饰的成员变量称为静态成员变量。
- 静态成员变量,属于类,不属于某个对象,为所有对象共享。
- 静态成员变量只能在类内部声明,在类外部初始化。
- 静态成员变量可以通过类名和对象名进行访问。
- 在为对象分配空间时不包括静态成员变量所占的空间。static修饰类的成员表示它们属于类本身,而不是类的对象。static成员可以是变量或函数,它们只有一个实例,可以在类外访问。
一个简单的例子是:
class Test { static int count; // 静态成员变量 public: Test() { count++; } static int getCount() { return count; } // 静态函数 }; int Test::count = 0; // 初始化静态成员变量 int main() { cout << "Initial count: " << Test::getCount() << endl; Test t1; Test t2; cout << "Final count: " << Test::getCount() << endl; }
输出:
Initial count: 0 Final count: 2
静态成员函数
静态成员函数是不需要对象就可以调用的函数,它们只能访问静态成员变量或枚举。静态成员函数可以在类内或类外定义,可以用类名或对象名调用。
一个例子是:
class IDGenerator { private: static int s_nextID; // static variable public: static int getNextID() { return s_nextID++; } // static function }; int IDGenerator::s_nextID = 1; // initialize static variable int main() { cout << "The next ID is: " << IDGenerator::getNextID() << endl; IDGenerator id; cout << "The next ID is: " << id.getNextID() << endl; }
输出:
The next ID is: 1 The next ID is: 2
-
2.9 友元
-
2.9.1 友元函数
有时候需要通过外部函数或类直接访问其他类的私有成员,为此C++提供了友元。
使用友元可以访问类中的所有成员,函数和类都可以作为友元。// 声明一个类 class Distance { private: int meter; public: Distance(): meter(0) { } // 声明一个友元函数 friend int addFive(Distance); }; // 定义一个友元函数 int addFive(Distance d) { // 可以访问私有成员 meter d.meter += 5; return d.meter; } int main() { Distance D; cout << "Distance: " << addFive(D); return 0; }
这个例子中,友元函数
addFive
可以访问并修改Distance
类的私有成员meter
。其他类的成员函数作为友元函数
其他类中的成员函数作为本类的友元函数时,需要在本类中表明该函数的作用域,并添加友元函数所在类(本类)的前向声明。class B;//类声明 class A{ public: int func();//成员函数 }; class B { friend int A::func();//友元函数 }
-
2.9.2 友元类
声明友元类之后,友元类中的所有成员函数都是该类的友元函数,能够访问该类的所有成员。
友元类可以声明在类中任意位置。
class B;//前向声明 class A{}; class B { void show(); friend class A;//声明类A是类B的友元类 }; //函数写在两类下面 void B::show(){ }
-
4 继承与派生
-
4.1 继承
-
4.1.1 继承的概念
继承是面向对象程序设计中的一个重要概念,它允许一个类(子类/派生类)从另一个类(父类/基类)继承属性和方法。这样可以增强代码的复用性。在 C++ 中,继承的语法是使用 : 符号
class 派生类名称:继承方式 基类名称 { 派生类成员声明 }
注意:
- 基类的构造函数与析构函数不能被继承
- 派生类对基类成员的继承没有选择权,不能选择
- 派生类中可添加新成员
- 一个基类可派生多个派生类,一个派生类可继承自多个基类继承关系图
- 空心箭头表示继承关系(派生类指向基类)
- “+”表示成员访问权限为“public”
- “-”表示成员访问权限为“privent”
- “#”表示成员访问权限为“protect”
-
4.1.2 继承方式
成员访问权限收成员自身访问权限的影响,还受继承方式的影响。
基类中:
- 公有:类内可 类外可
- 私有:类内可 类外不可
- 保护:类内可 类外不可派生类中:
// 父类 class Parent { private: int a; // 私有成员,只能在父类中访问 protected: int b; // 保护成员,可以在父类和子类中访问 public: int c; // 公有成员,可以在任何地方访问 }; // 子类1,公有继承 class Child1 : public Parent { // a 在这里不可访问 // b 在这里是保护的 // c 在这里是公有的 }; // 子类2,私有继承 class Child2 : private Parent { // a 在这里不可访问 // b 在这里是私有的 // c 在这里是私有的 }; // 子类3,保护继承 class Child3 : protected Parent { // a 在这里不可访问 // b 在这里是保护的 // c 在这里是保护的 };
-
4.1.3 类型兼容
在C++中,基类与派生类之间存在类型兼容
- 使用公有派生类对象为基类对象赋值
- 使用公有派生类对象为基类对象的引用赋值
- 使用公有派生类对象的地址为基类指针赋值
- 如果函数的参数是基类对象、基类对象的引用、基类指针、基类地址
-
4.2 派生类
-
4.2.1 派生类的构造与析构函数
派生类不会继承构造与析构函数
定义方式
派生类构造函数(参数列表):基类构造函数名(基类构造函数参数列表) { 派生类新增成员的初始化语句 }
注意:
- 系统在创建派生类对象时会自动调用基类构造函数和派生类构造函数(先调用父类再调用子类)
- 派生类构造函数的参数列表中需要包含派生类新增成员变量和基类成员变量的参数值。调用基类构造函数时,基类构造函数从派生类的参数列表中获取实参,因此不需要类型名。
- 如果基类没有构造函数或仅存无参构造函数,则在定义派生类构造函数时可省略对基类构造函数的调用
- 如果基类定义了有参构造函数,派生类必须定义构造函数,提供基类构造函数的参数,完成基类成员变量的初始化假设有一个基类
Animal
,有一个派生类Dog
,Dog
类有一个成员变量age
,则Dog
类的构造函数可以这样写:class Animal { public: Animal() {} }; class Dog : public Animal { public: Dog(int age) : Animal(), age_(age) {} private: int age_; };
在
Dog
类的构造函数中,必须调用基类的构造函数Animal()
,以初始化从基类继承的成员。同时,Dog
类的构造函数还要初始化Dog
类的成员变量age_
。#include using namespace std; class Engine { public: Engine(string type, int power); void show(); ~Engine(); private: string _type; int _power; }; Engine::Engine(string type, int power) { cout << "调用发动机Engine构造函数" << endl; _type = type; _power = power; } void Engine::show() { cout << "发动机型号:" << _type << ",发动机功率:"<<_power<<endl; } Engine::~Engine() { cout << "调用发动机Engine析构函数" << endl; } class Vehicle { public: Vehicle(string name); void run(); string getName(); ~Vehicle(); private: string _name; }; Vehicle::Vehicle(string name) { cout << "调用交通工具Vehicle构造函数" << endl; _name = name; } void Vehicle::run() { cout << _name << "正在行驶中" << endl; } string Vehicle::getName() { return _name; } Vehicle::~Vehicle() { cout << "调用交通工具Vehicle析构函数" << endl; } class Car :public Vehicle { public: Car(int seats, string color, string type, int power, string name); void brake(); void display(); ~Car(); Engine engine; private: int _seats; string _color; }; Car::Car(int seats, string color, string type, int power, string name) : engine(type, power), Vehicle(name) { cout << "调用小汽车Car构造函数" << endl; _seats = seats; _color = color; } void Car::brake() { cout << getName() << "停车" << endl; } void Car::display() { cout << getName() << "有" << _seats << "个座位," << "颜色为" << _color << endl; } Car::~Car() { cout << "调用小汽车Car析构函数" << endl; } int main() { Car car(5, "red", "EA113", 130, "passat"); //创建小汽车类对象car car.run(); //调用基类的run()函数 car.brake(); //调用brake()函数 car.display(); //调用display()函数 //通过成员对象engine调用Engine类的show()函数,显示发动机信息 car.engine.show(); return 0; }
运行结果:
调用交通工具Vehicle构造函数 调用发动机Engine构造函数 调用小汽车Car构造函数 passat正在行驶中 passat停车 passat有5个座位,颜色为red 发动机型号:EA113,发动机功率:130 调用小汽车Car析构函数 调用发动机Engine析构函数 调用交通工具Vehicle析构函数
-
4.2.2 在派生类中隐藏基类成员函数
在派生类中重新定义基类同名函数,基类同名函数在派生类中被隐藏。
- 通过派生类对象调用同名函数时,调用的是改写后的派生类成员函数,基类同名函数不会被调用。
- 如果想通过派生类对象调用基类的同名函数,需要使用作用域限定符“::”指定要调用的函数。注意:
- 只要是同名函数,无论参数列表和返回值类型是否相同,基类同名函数都会被隐藏。
- 若基类中有多个重载函数,派生类中有同名函数,则基类中所有同名函数在派生类中都会被隐藏。
-
4.3.1 多继承方式
多继承:基类有多个
继承方式:
class 派生类名 : 继承方式 基类1名称, 继承方式 基类2名称, …, 继承方式 基类n名称 { … 新增成员; … };
class Base1 { //基类Base1 protected: int base1; //成员变量base1 }; class Base2 { //基类Base2 protected: int base2; //成员变量base2 }; class Derive : public Base1, public Base2 { //Dervie类继承Base1类和Base2类 private: int derive; //派生类成员变量 };
构造函数的调用顺序是:首先按照基类继承顺序,依次调用基类构造函数,然后调用派生类构造函数。
-
4.3.2 基类成员的二义性
在多继承派生类中,若在多个基类中有同名的成员函数时,会产生成员的二义性
// 二义性.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include using namespace std; class Furniture { public: Furniture(string wood); protected: string _wood; }; Furniture::Furniture(string wood) { _wood = wood; } class Sofa :public Furniture { public: Sofa(float length, string wood); protected: float _length; }; Sofa::Sofa(float length, string wood) :Furniture(wood) { _length = length; }; class Bed :public Furniture { public: Bed(float width, string wood); protected: float _width; }; Bed::Bed(float width, string wood) :Furniture(wood) { _width = width; } class Sofabed :public Sofa, public Bed { public: Sofabed(float length, string wood1, float width, string wood2); void getSize(); }; Sofabed::Sofabed(float length, string wood1, float width, string wood2) : Sofa(length, wood1), Bed(width, wood2) {} void Sofabed::getSize() { cout << "沙发床长" << _length << "米" << endl; cout << "沙发床宽" << _width << "米" << endl; cout << "沙发床材质为" << _wood << endl; //报错--------------------------需改为Bed::_wood } int main() { Sofabed sbed(1.8, "梨木", 1.5, "檀木"); //创建Sofabed类对象sbed sbed.getSize();//调用getSize()函数获取沙发床信息 return 0; }
-
4.4 虚继承
在程序设计过程中,通常希望间接基类的成员变量在最底层派生类中只有一份拷贝,避免成员访问的二义性。
通过虚继承可以达到这样的目的。
虚继承就是在派生类继承基类时,在继承方式前加上virtual关键字,其格式如下所示:
class 派生类名:virtual 继承方式 基类名 { 派生类成员 };
// 二义性.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include using namespace std; class Furniture { public: Furniture(string wood); protected: string _wood; }; Furniture::Furniture(string wood) { _wood = wood; } class Sofa :virtual public Furniture { public: Sofa(float length, string wood); protected: float _length; }; Sofa::Sofa(float length, string wood) :Furniture(wood) { _length = length; }; class Bed :virtual public Furniture { public: Bed(float width, string wood); protected: float _width; }; Bed::Bed(float width, string wood) :Furniture(wood) { _width = width; } class Sofabed :public Sofa, public Bed { public: Sofabed(float length, string wood1, float width, string wood2); void getSize(); }; Sofabed::Sofabed(float length, string wood1, float width, string wood2) :Sofa(length,wood1),Bed(width,wood2),Furniture(wood1) {} void Sofabed::getSize() { cout << "沙发床长" << _length << "米" << endl; cout << "沙发床宽" << _width << "米" << endl; cout << "沙发床材质为" << _wood << endl; //不会报错--------------------------需改为Bed::_wood } int main() { Sofabed sbed(1.8, "梨木", 1.5, "檀木"); //创建Sofabed类对象sbed sbed.getSize();//调用getSize()函数获取沙发床信息 return 0; }
-
5 多态与虚函数
-
5.1 多态函数
静态多态:在编译阶段就能确定调用哪个函数,比如函数重载
动态多态:在程序运行过程中才确定调用哪个函数
一般多态函数指的是动态多态
多态是指同样的消息被不同类型的对象接收时导致不同的行为
-
5.2.1 虚函数
多态实现需要满足三个条件:
- 基类声明虚函数
- 怕派生类重写基类的虚函数
- 将基类指针指向派生类对象,通过基类指针访问虚函数注意:
- 派生类对积累虚函数重写时,必须与基类种虚函数的圆心完全一致
- 派生类种重写的虚函数前是否添加virtual,均被视为虚函数(通常都要加上)
-
5.2.2 虚函数实现多态的机制
在编写程序时,需要根据函数名、函数返回值类型、函数参数等信息正确的调用函数,这个匹配过程称为绑定。
- 静态绑定(静态联编,早绑定):编译器在编译时期就能确定要调用的函数
- 动态绑定(动态联编,迟绑定):编译器在运行时期才能确定要调用的函数虚函数就是通过动态绑定实现多态的。
多态:不同的对象对同一个消息产生了不同的行为
注意:
- 构造函数不能声明为虚函数,但析构可以
- 虚函数不能是静态成员函数1,override和final
-
5.2.3 虚析构函数
格式:
virtual ~析构函数();
基类的所有派生类的析构函数都自动成为虚析构函数
-
5.3
纯虚函数与抽象类纯虚函数也是通过virtual关键字声明,没有函数体
作用:
- 在基类中为派生类保留一个接口,方便派生类更具需要完成定义,实现多态
- 派生类都应该实现基类的纯虚函数如果一个类中包含纯虚函数,这样的类称为抽象类。
注意:
- 抽象类只能作为基类派生新类,不能创建抽象类的对象,但可以定义抽象类的指针或引用。
//例5-3 #include using namespace std; class Animal {//动物类Animal public: virtual void speak() = 0;//纯虚函数 virtual void eat() = 0;//纯虚函数 virtual ~Animal();//析构函数 }; Animal::~Animal() { cout << "调用Animal析构函数" << endl; } class Cat :public Animal {//猫类Cat公有继承Animal类 public: void speak();//声明speak()函数 void eat();//声明speak()函数 ~Cat();//声明析构函数 }; void Cat::speak() { cout << "小猫喵喵叫" << endl; } void Cat::eat() { cout << "小猫吃鱼" << endl; } Cat::~Cat() { cout << "调用Cat析构函数" << endl; } class Rabbit :public Animal {//兔子类Rabbit公有继承Animal类 public: void speak();//声明speak()函数 void eat();//声明eat()函数 ~Rabbit();//声明析构函数 }; void Rabbit::speak() { cout << "小兔子咕咕叫" << endl; } void Rabbit::eat() { cout << "小兔子吃白菜" << endl; } Rabbit::~Rabbit() { cout << "调用Rabbit析构函数" << endl; } int main() { Animal* pC = new Cat;//定义基类指针pC指向Cat类对象 pC->speak();//通过pC指针调用speak()函数 pC->eat();//通过pC指针调用eat()函数 delete pC;//释放指针pC指向的空间 Animal* pR = new Rabbit;//定义基类指针pR指向Rabbit类对象 pR->speak();//通过pR指针调用speak()函数 pR->eat();//通过pR指针调用eat()函数 delete pR;//释放pR指向的空间 return 0; }
运行结果:
小猫喵喵叫 小猫吃鱼 调用Cat析构函数 调用Animal析构函数 小兔子咕咕叫 小兔子吃白菜 调用Rabbit析构函数 调用Animal析构函数
例:
请设计一个描述形状的抽象类Shape,Shape类种有两个成员函数:
(1)getArea()函数:用于计算形状的面积
(2)getLen()函数:用于计算形状的周长
Shape类有两个派生类:
(1)Rectangle类:表示矩形
(2)Circle类:表示圆形
请编写程序实现一下功能:
(1)计算不同边长的矩形的面积和周长
(2)计算不同半径的元的面积和周长
答:
#include using namespace std; class Shape { public: virtual void getArea() = 0; virtual void getLen() = 0; } class Rectangle :public Shape { public: int _len; int _weight; } void rectangle::getArea(){ }
-
3 运算符重载
-
3.1 运算符重载概述
运算符重载就是对现有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
定义:
返回值类型 operator 运算符名称(参数列表) { ...//函数体 }
规则:
- 只能重载C++中已有的运算都,不能创建新的运算符。
- 重载后运算符不能改变优先级和结合性,也不能改变C++中可以重载的运算符包括:+、-、、/、%、^、&、|、~、!、=、<、>、+=、-=、=、/=、%=、^=、&=、|=、<<=、>>=、==、!=、<=和>=等。需要注意的是,并不是所有的运算符都可以重载,例如作用域解析运算符(::)和三目运算符(?:)等就不能被重载。
-
3.3 运算符重载的形式
1,重载为类的成员函数
(1)如果是双目运算符重载为类的成员函数,则它又两个操作数:左操作数
2,重载为类的友元函数
-
6 模板
在C++中,模板是一种支持参数化多态的工具,使用模板可以使用户为类或函数声明一种一般模式,使得类中的某些数据成员或成员函数的参数、返回值取得任意类型¹²。
模板是一种对类型进行参数化的工具,通常有两种形式:函数模板和类模板²⁵。函数模板是一种通用函数的定义,可以用于多种不同的数据类型;类模板是一种通用类的定义,可以用于多种不同的数据类型⁵。
需要注意的是,模板并不是代码,而是代码生成器。在编译时,编译器会根据使用模板的情况生成相应的代码¹。
(1) C++ 模板详解 | 菜鸟教程. https://bing.com/search?q=C%2B%2B中模板的概念
(2) C++ 模板详解 | 菜鸟教程. https://www.runoob.com/w3cnote/c-templates-detail.html
(3) C++ 模板 | 菜鸟教程. https://www.runoob.com/cplusplus/cpp-templates.html
(4) C++模板template用法 – 知乎. https://zhuanlan.zhihu.com/p/77241939
(5) C++模板的概念 定义和使用_这是一个死肥宅的博客-CSDN博客. https://blog.csdn.net/qq_28840013/article/details/84844300 -
6.2 函数模板
例:
隐式实例化:
template T add(T t1, T t2) { return t1 + t2 } int main () { cout << add(1, 2) << endl; cout << add(1.2, 3.4) << endl; return 0; }
结果:
3 4.6
显式实例化:
template T add(T t1, T t2) { return t1 + t2 } int main () { cout << add(10, 'B') << endl; cout << add(1.2, 3) << endl; return 0; }
结果:
67 4
-
6.3 类模板定义与实例化
例:
template class A { public: T a; T b; T func(T a, T b); }
-
8 I/O流
标准i/o流,文件流,字符串流
io:
流:
-
8.1 i/o流类库
ios类库,streambuf类库
-
8.1.1 ios类库
- ifstream类
- istringsteram类
- ofstream类
- ostringsteram类
- fstream类
- stringsteram类
-
8.1.2 streambuf类库
- stdiobuf类
- filebuf类
- stringstreambuf类
-
8.2 标准i/o流
-
8.2.1 预定义流对象
C++提供了四个预定义流对象:************************************cin、cout、cerr、clog。
-
8.2.2 标准输出流
1,put函数:
ostream& put(char ch)
例:
int main () { cout.put('a'); cout.put('\n'); cout.put('b').put('c'); return 0; }
2,write函数
int main () { cout.write("I love China",6); return 0; }
结果:
I love
-
8.2.3 标准输入流
get函数:
- 无参数的get()函数:其调用形式为cin.get(),用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符。
- 有一个参数的get()函数:其调用形式为cin.get(char& ch),用来从指定的输入流中提取一个字符(包括空白字符),并将其存储到ch中,函数的返回值就是读入的字符。
- 有两个参数的get()函数:其调用形式为cin.get(char* s, int n, ‘ ’),用来从指定的输入流中提取n-1个字符(包括空白字符),并将其存储到s所指向的字符数组中,最后再在末尾添加一个空字符’\0’,函数的返回值是读入的字符数。
#include using namespace std; int main() { char ch; char str[20]; int n; // 无参数的get()函数 ch = cin.get(); cout << "无参数的get()函数读入的字符为:" << ch << endl; // 有一个参数的get()函数 cin.get(ch); cout << "有一个参数的get()函数读入的字符为:" << ch << endl; // 有两个参数的get()函数 cin.get(str, 20); cout << "有两个参数的get()函数读入的字符串为:" << str << endl; return 0; }
getline函数:
read函数:
不会识别换行符、空格等特殊字符。即使遇到换行也不会结束读取。
-
9 异常
导致程序运行失败的错误,通常称为异常
应对异常:异常处理机制
-
9.1 异常处理方式
throw关键字和try…catch语句
throw和try…catch要同时使用,除非抛出标准异常
throw关键字抛出异常的语法格式:
throw 表达式;
表达式可以是常量,变量或对象。
try…catch:
try{...}//可能出现异常的代码 catch(异常类型1){...}//对异常的处理 catch(异常类型2){...}//对异常的处理 catch(异常类型3){...}//对异常的处理 ... catch(异常类型n){...}//对异常的处理 catch(...){...}//可捕获任意类型的错误
try检测可能发生异常的代码
catch会一次对抛出的异常进行类型匹配。
一旦某个catch捕获到了异常,后面的catch将不再执行
try和catch不能分开用,必须连起来使用
-
9.2 栈解旋
在异常处理前释放(析构)所有的局部对象
-
9.3 标准异常
-
7 STL(标准模板库)
可重复利用
复用性
从广义上分为三大类:容器 算法 迭代器。
基本理念:将数据和操作分离
-
7.2 容器
序列容器(顺序容器):各元素之间有顺序关系
特点:能在两端增加或删除元素
vector容器:在插入或删除元素是能够自动调整自身大小
插入:插入卫视之后的元素要被顺序地向后移动
删除:。。。。。。。。。。。。。。向前移动
方法:
vector<元素类型> 对象名(容器大小) vector<元素类型> 对象名(容器大小,元素初始值) vector<元素类型> 对象名 = {'1','2',...} vector<元素类型> 对象名1(对象名2) vector<元素类型> 对象名1 = 对象名2
e.g.
vectorv1(10,2);
#更新日期:2023/4/27 #### ### ##### ###### #####