目录
一、类与结构体
1.类与结构体
1、类与结构体的主要区别:
- 类是引用类型,结构是值类型。
- 结构不支持继承。
- 结构不能声明默认的构造函数。
2、结构和类的适用场合:
- 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构好一些;
- 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低;
- 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构不支持继承。
2.new与delete
1、new的用法:
- new int;(开辟一个存放整数的存储空间,返回一个指向该存储空间的地址)
- new int(100);(开辟空间,并初始化整数为100,返回地址)
- new char[10];(开辟字符数字空间,返回首元素地址)创建数组对象时,不能为对象指定初始值。
- new int[5][5];(开辟二维数组空间,返回首元素地址)
- float *p=new float(3.14159);(开辟空间,赋初值,并将地址赋给指针变量p)
2、delete的用法:释放已分配的空间
- (delete 指针变量)(delete[] 指针变量)(指针变量必须是一个new返回的地址)
3.类的封装访问属性
1、结构体能做的事类都能做,类能做的事结构体不一定能做(本质是:结构体内不能封装函数,但类可以)(故在c++中学会了类也就学会了结构体)
2、封装对内开放数据,对外屏蔽数据,对外提供接口,从而达到信息的隐蔽作用。
3、封装访问属性:
- 用struct定义类的时候,其所有成员默认为public。
- 用class定义类的时候,其所有成员默认为private。
|---------------|--------|----------|----------|
| 访问属性 | 属性 | 对象内部 | 对象外部 |
| public | 公有 | 可访问 | 可访问 |
| protected | 保护 | 可访问 | 不可访问 |
| private | 私有 | 可访问 | 不可访问 |
二、类的封装
1.构造函数
1、构造函数:
- 构造函数在对象创建时自动调用且只调用一次**,完成初始化相关工作。**
- 无返回值,与类名相同,默认无参数,可以重载,可以默认有参数。
- 一经实现,默认将不复存在。
- 拷贝构造函数的参数一定是引用类型。
2、无参构造函数、带参构造函数、拷贝构造函数的实现。
cpp#include<iostream> using namespace std; class Person { public: int age; Person()//无参构造函数 { cout << "Person 无参构造函数的调用" << endl; } Person(int a)//有参构造函数 { age = a; cout << "Person 有参构造函数的调用" << endl; } Person(const Person &p)//拷贝构造函数 { //将传入的人身上的所有属性,拷贝在我的身上 age = p.age; cout << "Person 拷贝构造函数的调用" << endl; } //析构函数不可以有参数,不可以发生重载 //对象销毁前 会自动调用析构函数 而且只会调用一次 ~Person() { cout << "Person 析构函数的调用" << endl; } }; void test01() { Person p1; //默认构造函数的调用 Person p2(10); //有参构造函数的调用 Person p3(p2); //拷贝构造函数的调用 //注意事项1:调用默认构造函数时候,不用加() //因为下面这行代码,编译器会认为是一个函数的声明 //Person p1(); cout << "p2的年龄为:" << p2.age << endl; cout << "p3的年龄为:" << p3.age << endl; } int main() { test01(); system("pause"); return 0; }
cpp//运行结果: Person 无参构造函数的调用 Person 有参构造函数的调用 Person 拷贝构造函数的调用 p2的年龄为:10 p3的年龄为:10 Person 析构函数的调用 Person 析构函数的调用 Person 析构函数的调用
2.析构函数
1、析构函数
- 构造函数是在实例化的时候被调用,而析构函数在对象销毁的时候被调用。
- 在对象销毁前会自动调用析构函数,而且只会调用一次,目的时清空数据,释放内存,防止内存外泄。
- 无返回值,与类名相同,前面带一个 ~ 符号,无参数,不可重载默认参数。
- 析构函数的作用并不是删除对象,而是在对象销毁前完成一些清理工作。
2、析构函数的实现
cpp#include<iostream> using namespace std; class Person { public: Person() { cout << "Person 构造函数的调用" << endl; } //1.2、析构函数没有返回值 不写void //析构函数名和类名相同 在名称前加~ //析构函数不可以有参数,不可以发生重载 //对象销毁前 会自动调用析构函数 而且只会调用一次 ~Person() { cout << "Person 析构函数的调用" << endl; } }; void test01() { Person P; } int main() { test01(); system("pause"); return 0; }
3.静态成员
1、静态成员分为静态成员变量和静态成员函数。静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
2、静态成员属于整个类而不是某个对象,类的静态成员属于类也属于对象,最终归于类。
3、静态成员变量
- 所有对象共享同一份数据,实现了同类对象的信息共享;
- 在编译阶段分配内存;
- 类内声明,类外初始化(类外存储,求类大小,并不包括在内);
- 可以通过类名访问,也可通过对象访问。
4、静态成员的实现
- 静态成员函数只能访问静态成员变量,无法访问非静态成员变量。 (因为非静态成员函数在调用的时候this指针会被当作函数参数进行传递,而静态成员函数属于类不属于对象,没有this指针,故无法进行访问)
- private权限,类外无法访问。
- 不在类内部初始化 。
- 静态函数意义不是信息共享,而在于数据的沟通,管理静态数据成员,完成对静态数据成员的封装。
cpp#include<iostream> using namespace std; class person { public: static int m_A; static void func() { m_C = 100; // m_D = 200;报错,在类中m_B无法赋值,因为静态成员函数只能访问静态成员变量; cout << "func函数" << endl; } static int m_C; //不在类内部初始化 int m_D; private: static int m_B;//private权限,类外无法访问 static void func2()//func2()是private,类外无法访问 { cout << "func2函数" << endl; } }; //在类外部初始化静态成员变量 int person::m_A = 100; int person::m_B = 200; int person::m_C = 0; int main() { person p; cout << p.m_A << endl; cout << person::m_A << endl; // cout << person::m_B << endl;此处报错,private权限类外无法访问 p.func(); person::func(); // p.func2();// 注意:func2()是private,无法从 main 直接调用 return 0; }
4.友元
1、友元分为:友元函数和友元类。 (目的:提高程序的运行效率,但是破坏了类的封装性和隐蔽性,使得非成员函数可以访问类的私有函数)
2、友元函数、友元类定义(友元=友元函数+友元类):
- 友元的声明位置没有要求,可以在private、protected、public权限区,效果都是一样的;
- 友元是单向的,A在B类中被声明为友元,表示A是B的友元,但B不是A的友元;
- 友元具有和类成员一样的权限,可以访问protected和private权限的成员,但不是类的成员;
3、友元函数
- 友元函数在类中声明时用friend修饰,但是在定义时不需要用friend修饰;
- 友元函数不能被继承:父类的友元函数,继承后并不会成为子类的友元函数;
- 友元函数不具有传递性:A类和B类都是C类的友元类,但是A类和B类并不是友元类;
4、优缺点:
- 缺点:友元函数不是类的成员但是却具有成员的权限,可以访问类中受保护的成员,这破坏了类的封装特性和权限管控;(代码更灵活,但是不能滥用)
- 优点:可以实现类之间的数据共享;
5、友元函数的实现
cpp#include <iostream> using namespace std; class Person{ private: int age; public: Person(){}; Person(int x); friend void print(Person &pn);//声明print是友元函数 }; Person::Person(int x){ this->age = x; } void print(Person &pn){ //因为print是Person类的友元函数,所以在内部可以访问Person类的私有成员age cout << "age=" << pn.age << endl; } int main(void) { Person p(22); print(p); return 0; }
6、友元类的实现
cpp#include <iostream> using namespace std; class Person { private: int age; public: Person() {} Person(int x) : age(x) {} // 使用初始化列表初始化age friend class Printer;// 声明Printer类为Person的友元类 }; class Printer { public: // 因为Printer是Person的友元类,所以可以访问Person的私有成员 void print(Person &pn) { cout << "age=" << pn.age << endl; } }; int main(void) { Person p(22); Printer printer; printer.print(p); return 0; }
5.运算符重载
1、重载的理解:
- 在一个作用域内,可以声明几个功能类似的同名函数(也就是"一名多用"),但这些同名函数的形式参数(参数的个数、类型或顺序)必须不同。
2、总结重载:
- 函数名相同。
- 参数个数不同,参数类型不同,参数顺序不同,均可以构成重载。
- 如果只有返回值类型不同的时候则不可以构成重载。
3、运算符重载:运算符重载的实质就是函数重载。
- 就是编译软件本身的运算符不符合我们想要的运算符定义,我们需要重新定义运算符来达到我们的目的。
- 运算符重载格式类型:
cpp返回类型 operator 运算符(参数列表) { 函数体 ; }
4、重载运算符规则:
C++中不允许用户定义新的运算符,只能对已有的运算符进行重载。
重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语法结构。
重载后的含义,与操作基本数据类型的运算含义应类似,如加法运算符"+",重载后也应完成数据的相加操作。
有5个运算符不可重载:类关系运算符":"、成员指针运算符"*"、作用域运算 符"::"、sizeof运算符、三目运算符"?:"
运算符重载函数不能有默认参数,否则就改变了运算符操作数的个数,是错误的。
用于类对象的运算符一般必须重载,但有两个例外("="和"&"不必重载)。
运算符重载函数既可以作为类的成员函数,也可以作为类的友元函数(全局函数)。
三、类的继承
1.继承和派生
1、继承:所谓继承就是在一个已存在的类的基础上建立一个新的类。
- 在继承关系中被继承的类称为基类(或父类),把通过继承关系创建出来的新类称为派生类(子类)。
- 派生类对基类对象的访问由继承方式和成员性质决定。
- 利用类的继承,可以将原来的程序代码重复使用,从而减少了程序代码的冗余度,符合软件重用的目标,提高软件开发效率。
- 派生类不仅可以继承原来类的成员,还可以**增加新的数据成员、增加新的成员函数、**重新定义已有成员函数、改变现有成员的属性。
- 继承具有传递性,即派生类能自动继承上层基类的全部数据结构及操作方法(数据成员及成员函数)
2、继承与派生的对应关系:
- 一个派生类只从一个基类派生,这是最常见的继承形式
- 一个派生类有两个及两个以上的基类。如:类C从类A和类B派生。
3、继承方式有public(公有继承)、protected(保护继承)和private(私有继承)
- **public(公有继承):**基类的公有成员和保护成员被继承为派生类成员时,其访问属性不变。
- protected(保护继承):基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有。
- private(私有继承):基类中的公有成员和保护成员在派生类中皆变为私有成员。
- 无论哪种继承方式,基类的私有成员均不能继承。这与私有成员的定义是一致的,符合数据封装的思想。
2.继承中的构造与析构
1、构造顺序(析构是相反的顺序):
- 先构造父类,再构造成员变量,最后构造自己。
- 先构造自己,再构造成员变量,最后构造父类。
cpp#include <iostream> using namespace std; //基类 Object class Object { public: Object(const char* s) { cout << "Object(" << s << ")" << endl; } ~Object() { cout << "~Object()" << endl; } }; //派生类 Parent,继承自 Object class Parent : public Object { public: Parent(const char* s) : Object(s) { cout << "Parent(" << s << ")" << endl; } ~Parent() { cout << "~Parent()" << endl; } }; //派生类 Child,继承自 Parent class Child : public Parent { Object o1; // 成员变量 Object o2; // 成员变量 public: Child(const char* s) : Parent(s), o1("o1"), o2("o2") { //按照声明顺序初始化成员变量 o1 和 o2(每个都会调用 Object 的构造函数) cout << "Child(" << s << ")" << endl; } ~Child() { cout << "~Child()" << endl; } }; int main() { Child c("Parameter from Child!"); // 析构顺序与构造顺序相反 return 0; }
cpp//输出结果如下所示: Object(Parameter from Child!) Parent(Parameter from Child!) Object(o1) Object(o2) Child(Parameter from Child!) ~Child() ~Object() ~Object() ~Parent() ~Object()
1、构造函数调用顺序:
- 首先调用基类 Object 的构造函数(通过 Parent 的构造函数传递参数)。
- 然后调用 Parent 的构造函数。
- 接着,在 Child 的构造函数体内,按照声明顺序初始化成员变量 o1 和 o2(每个都会调用 Object 的构造函数)。
- 最后,调用 Child 的构造函数体。
2、析构函数调用顺序:
- 与构造函数顺序相反,首先调用 Child 的析构函数。
- 然后是成员变量 o2 和 o1 的析构函数(按照声明顺序的逆序)。
- 接着是 Parent 的析构函数。
- 最后是 Object 的析构函数(在 Parent 的析构函数中隐式调用,因为 Parent 继承自 Object)。
3.多继承中的二义性
1、继承中的二义性问题所在:
- 相当于初始化了2个变量,而且地址不同,互相不影响,因此不知道访问哪个地址的变量,进而出现二义性的问题。
- 解决二义性的两种方法:一种是直接限定空间区域、一种是虚继承。
- C++提供虚继承机制,就是防止继承关系中成员访问的二义性。
- 多继承提供了软件重用的强大功能,也增加了程序的复杂性。
cpp#include <iostream> class Base1 { public: void show() { std::cout << "Base1::show()" << std::endl; } }; class Base2 { public: void show() { std::cout << "Base2::show()" << std::endl; } }; class Derived : public Base1, public Base2 { public: void test() {//这里出错了 show(); //尝试调用show,这里将产生二义性错误 //相当于初始化了2个变量,而且地址不同,互相不影响,因此不知道访问哪个地址的变量 } }; int main() { Derived d; d.test(); return 0; }
2、直接限定空间区域方法
cpp#include <iostream> class Base1 { public: void show() { std::cout << "Base1::show()" << std::endl; } }; class Base2 { public: void show() { std::cout << "Base2::show()" << std::endl; } }; class Derived : public Base1, public Base2 { public: void test() { Base1::show(); // 调用Base1的show Base2::show(); // 调用Base2的show } }; int main() { Derived d; d.test(); return 0; }
3、虚继承方法
cpp#include <iostream> class Base { public: void showBase() { // 假设我们还有一个不同的函数来展示虚继承的效果 std::cout << "Base::showBase()" << std::endl; } }; class Base1 : virtual public Base { // 虚继承自Base public: void show() { std::cout << "Base1::show()" << std::endl; showBase(); // 调用Base的showBase } }; class Base2 : virtual public Base { // 虚继承自Base public: void show() { std::cout << "Base2::show()" << std::endl; showBase(); // 调用Base的showBase } }; class Derived : public Base1, public Base2 { public: // 使用作用域解析运算符解决二义性 void test() { Base1::show(); // 调用Base1的show Base2::show(); // 调用Base2的show } }; int main() { Derived d; d.test(); return 0; }
四、类的多态
1.多态
**1、C++中的多态:**由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。
2、多态成立的条件(缺一不可):------>(结果:派生类函数覆盖父类函数)
- 要有继承(父类到子类之间的继承)
- 要有虚函数重写(有关键字virtual关键字)
- 要有父类指针(父类引用)指向子类指针
3、多态实现
cpp#include <iostream> using namespace std; // 定义基类 class Parent { public: virtual void print() { // 声明为虚函数 cout << "Parent:print() do." << endl; } virtual ~Parent() {} // 虚析构函数,用于安全删除派生类对象 }; // 定义派生类 class Child : public Parent { public: void print() override { // 重写基类的虚函数 cout << "Child:print() do." << endl; } }; // 定义一个函数,接收Parent类型的指针 void function(Parent* p) { p->print(); // 通过基类指针调用虚函数,实现多态 } int main() { // 首先是普通调用 // 输出Parent:print() do. 多态未发生,因为这里的指针指向的是基类对象 Parent* pp = new Parent(); pp->print(); // 多态发生,满足条件:基类指针指向派生类对象 Parent* pc = new Child(); pc->print(); // 输出:Child:print() do. // 在函数中作用体现更明显 Parent* pcf = new Child(); function(pcf); // 输出:Child:print() do. //代码逻辑: //当基类指针 pc 或 pcf 指向一个 Child 类的对象时,调用 pc->print() 或 pcf->print() //会执行 Child 类的 print() 函数。这是因为虽然指针的类型是 Parent*, //但它实际上指向的是一个 Child 类的对象。由于 print() 是虚函数, //所以在运行时,C++ 运行时系统会根据指针实际指向的对象的类型 //来确定调用哪个版本的 print() 函数。这就是多态性的体现。 return 0; }
2.函数的重载、重写、和重定义
1、重载(Overloading)
定义 :在同一个作用域内,允许存在多个同名函数,只要这些函数的参数列表不同(参数个数、类型或顺序不同),则这些函数就被称为重载函数。重载与函数的返回类型无关。
特点:
函数名相同。
参数列表不同(包括参数个数、类型或顺序)。
允许存在于同一个类或同一个命名空间中。
调用时根据参数列表选择对应的函数版本(编译时多态)。
2、重写(Overriding)
定义 :派生类函数覆盖基类函数。
特点:
基类函数必须是虚函数。
派生类函数与基类函数在名称、参数列表和返回类型上必须完全匹配。
允许通过基类指针或引用来调用派生类中的函数(运行时多态)。
3、重定义(Hiding)
定义:也称为函数隐藏,当派生类中的函数与基类中的函数同名时,如果这两个函数的参数列表不同,或者基类函数没有声明为虚函数,即使参数列表相同,派生类中的函数也会隐藏基类中的同名函数。
特点:
派生类函数与基类函数同名。
参数列表可能相同或不同。
如果基类函数没有声明为虚函数,即使参数列表相同,基类函数也会被隐藏。
隐藏与多态无关,它发生在编译时,编译器根据对象的静态类型(即编译时类型)来确定调用哪个函数。
总结
重载是编译时多态的一种体现,允许在同一作用域内定义多个同名但参数列表不同的函数。
重写是运行时多态的基础,通过基类指针或引用来调用派生类中的函数,前提是基类函数必须是虚函数。
重定义(隐藏)可能导致意外的行为,特别是当基类函数被设计为通过基类指针调用时。它发生在编译时,且不受基类函数是否为虚函数的影响。