4 类(class)和对象(object)
C++面向对象的三大特性:封装、继承、多态
C++认为万事万物皆对象,对象上有其属性和行为
对于一些具有相同性质的对象,我们可以抽象称他们为类
4.1 封装
4.1.1 封装的意义
封装是C++面向对象三大特性之一
意义:
-
在设计类的时候,将属性和行为写在一起,表现生活中的事物
语法:
class 类名{ 访问权限 : 属性 / 行为}; -
设计类时,可以把属性和行为放在不同的权限下加以控制
访问权限有三种:
- public 公共权限 成员 类内可以访问,类外可以访问
- protected 保护权限 成员 类内可以访问,类外不可访问(子类可以访问父类中的保护内容)
- private 私有权限 成员 类内可以访问,类外不可访问(子类不可以访问父类中的私有内容)
所谓的“类内可访问、类外不可访问”指的是,对应的属性和行为在类内使用方法可以随意访问,但是在主函数区域中通过实例化类的方式是不能访问的。
通过类创建具体的对象的过程叫做实例化
实例化的两种形式:
-
在栈区实例化
class 类名 变量名; //与struct一样,class可以被省略 类名 数组名[100]; //创建单个对象外还可以创建对象数组通过这种方式创建的对象,访问其成员变量和成员函数时使用
.操作符 -
在堆区实例化
类名 *指针名 = new 类名; delete 指针名;使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。使用完毕后需要delete释放内存;且访问成员变量和成员函数时使用
->操作符。
类中的属性和行为统称为成员
属性
成员属性
成员变量
行为
成员函数
成员方法
4.1.2 struct和class的区别
在C++中struct和class的唯一区别就在于默认的访问权限不同
区别:
struct默认权限为公共
class默认权限为私有
4.1.3 成员属性设置为私有
优点:
-
将所有成员属性设置为私有,可以自己控制读写权限
class person{ public: //以预留接口的方式对类内的私有属性进行读写操作,并可以控制读写权限 string setName(string name){ m_name = name; return m_name; } private: string m_name; //类外不能访问 } -
对于写权限,我们可以检测数据的有效性
class person{ public: void setAge(int age){ //可以对写入数据的有效性进行判断 if (age < 0 || age > 150){ printf("非法输入"); return; } m_Age = age; } private: int m_Age; }
4.1.4 番外:头文件(.h)和源文件(.cpp)的编写
头文件(.h):写类的声明(包括类中成员和方法的声明)、函数原型、#define常数等,一般来说不写出具体实现。
在开头和结尾处需按照以下样式加入预编译语句,以防止重复编译而出错。
#ifndef 类名
#define 类名
//代码(类的声明等)
#endif
但这种方式在遇到不同头文件中的宏名相同时,会出现找不到声明的情况;且每次都需要打开头文件才能判定是否有重复定义,所以在编译大型项目时,#ifndef会使编译时间相对较长。
因此,现有的编译器大多支持#pragma once的方式,编译器会保证同一个文件(物理意义上的文件、而非内容相同的两个文件)不会被包含多次。但无法对头文件中的一段代码做#pragma once声明,只能针对文件。
#pragma once
//文件内容
源文件(.cpp):源文件的内容主要是实现头文件中已声明函数的具体代码,在开头必须#include实现的头文件,以及要用到的头文件。
e.g:
头文件编写Circle.h
//#pragma once
#ifndef CIRCLE_H //CIRCLE_H其实也可以换成其他名字,但这么写可以方便与头文件相对应
#define CIRCLE_H
class Circle{
private:
double r;
public:
Circle1();
Circle1(double R);
double Area();
};
#endif
源文件编写Circle.cpp
#include "Ciecle.h"
Circle::Circle1(){ //"Circle::"是表明当前函数的作用域
//this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
this->r=5.0; //this是一个const指针,指向当前的对象,通过此指针可以访问当前对象的所有成员
}
Circle::Circle1(double r){
this->r=r; //使用this可以在成员函数的参数与成员变量重名的情况下加以区分
}
double Ciecle::Area(){
return 3.14*r*r;
}
4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
若一个对象或变量没有初始状态,其使用结果是未知;使用完一个对象或者变量若没有及时清理,也会造成一定的安全问题。
C++中使用构造函数和析构函数来解决上述问题,这两个函数会被编译器自动调用,完成对象的初始化和清理。如果不提供构造和析构,编译器会提供,但编译器提供的构造函数和析构函数是空实现(空实现其实就是指函数中没有任何操作)
- 构造函数:主要作用在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
- 析构函数:主要作用在对象销毁前,系统会自动调用以执行一些清理操作
构造函数语法:类名(){}
- 构造函数没有返回值,也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造,无需手动调用,且仅调用一次
析构函数语法:~类名(){}>
- 析构函数没有返回值,也不写void
- 函数名称与类名相同,在名称前加上
~符号 - 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,且仅调用一次
4.2.2 构造函数的分类与调用
两种分类方式:
-
按参数分为:有参构造和无参构造(默认构造)
-
按类型分为:普通构造和拷贝构造
拷贝构造的语法:类名(const 类名 &形参名){},实际上是将传入对象的属性复制给当前对象
class Person{
public:
//无参构造(默认构造)
Person(){
}
//有参构造
Person(int a){
age = a;
}
//拷贝构造
Person(const Person &p){
age = p.age;
}
int age;
}
三种调用方式:
-
括号法
Person p1; //无参 Person p2(10); //有参 Person p3(p1); //拷贝在调用默认构造函数的时候不要加(),否则编译器会认为是在声明函数,而非创建对象,如
Person P1(); -
显示法
Person P1; //无参 Person p2 = Person(10); //有参 Person p3 = Person(p2); //拷贝 Person(p3); //非法操作,匿名对象使用拷贝构造-
Person(p2);、Person(10);属于匿名对象,匿名对象在该行结束后,系统就自动回收该对象(析构) -
不能用拷贝构造函数初始化匿名对象,如
Person(p3),编译器认为Person(p3);是在调用无参构造Person p3;,会产生重定义的错误。
-
-
隐式转换法
Person p4 = 10; //有参构造,相当于写了Person p4 = Person(10); Person p5 = p4; //拷贝构造使用等号的隐式转换法,将实参作为右值,编译器会自动完成右值到构造函数实参的转换
4.2.3 拷贝构造函数
Person(const Person &p){
m_a = p.m_a;
}
对象的创建分两阶段:分配内存->初始化(调用构造函数,拷贝发生)
拷贝构造函数有两点需要注意:
-
参数必须是当前类的引用而不是当前类的对象
这是由于调用拷贝构造函数,将对象传递给形参时本身就是一次拷贝,引用了拷贝构造函数,而调用拷贝构造函数的时候又会将一个对象传递给形参……进入死循环;只有当参数是当前类的引用的时候才不会再次调用,避免循环。
-
参数必须是const修饰的常量引用
调用拷贝函数仅仅希望复制属性,不想改变值,使用const含义更明确;
使用const限制,const对象和非const对象都可以传递给形参(非const型可以转换为const型);但没有const限制时,只能传入非const型作为形参(const型不可以转换为非const型)
拷贝构造函数的三种调用方式:
-
使用一个已经创建完毕的对象来初始化一个新的对象
-
值传递的方式给函数参数传值
void test01(Person a){ } void test02(){ Person p; test01(p); }在函数使用值传递的方式将对象作为实参传递给形参时,会使用拷贝传递。
-
以值的方式返回局部对象(与编译器有关,并不一定发生)
在函数要将对象作为返回值时,有些编译器会使用拷贝传递的方式,建立一个临时对象将返回对象拷贝,之后再传递给外部;但现有编译器可能会进行返回值优化,不再进行拷贝。
4.2.4 构造函数调用规则
默认情况下C++至少为一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造(默认将所有属性进行复制)
- 如果用户定义拷贝构造函数,C++不再提供其他构造函数
4.2.5 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作(编译器默认提供的拷贝构造),可能造成堆区内存的重复释放
深拷贝:在堆区重新申请空间,进行拷贝操作(自己写的构造函数u,自行在堆区申请新的内存)
class Person{
public:
int age;
int *height;
Person(int a, int b){
age = a;
int *height = new int(b);
}
Person(const Person &p){
age = p.age;
height = p.height; //浅拷贝
height = new int(*p.height); //深拷贝
}
~Person(){
if(height!=NULL){
delete height;
height = NULL;
}
}
};
void test(){
Person A(10, 166);
Person B(A);
}
两种拷贝方式的结果在使用一个对象B 拷贝对象A 时尤为明显,由于默认浅拷贝是简单的赋值相等,因此AB的属性指针height会指向同一个地址,在B析构完后该内存地址为空,A无法再次析构。
如果属性中有在堆区中开辟的,那么一定需要自己提供拷贝构造函数,防止浅拷贝带来的问题。
4.2.6 初始化列表
初始化列表语法用于初始化属性
在定义的同时进行赋值叫做初始化(initialization),定义完成后再赋值叫做赋值(assignment)。初始化只能由一次,但赋值可以有多次。
语法:构造函数(): 属性1(值1),属性2(值2)…{}
class Person{
public:
//传统参数构造
Person(int a, int b, int c){
m_A = a;
m_B = b;
m_C = C;
}
//初始化列表
Person():m_A(值1),m_B(值2),m_C(值3){}
//初始化列表优化,上面的方法只能用于指定的值123,优化后可以传参
Person(int a, int b, int c):m_A(a), m_B(b), m_C(c){}
int m_A;
int m_B;
int m_C;
};
- 成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
- const成员变量初始化只能使用初始化列表。
4.2.7 拷贝控制操作(C++三五法则)
- 当定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作,分别是拷贝构造函数、赋值运算符和析构函数。
- 拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么,赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么,析构函数定义了此类型的对象销毁时做什么。我们将这些操作称为拷贝控制操作。
- 由于拷贝控制操作是由三个特殊的成员函数来完成的,所以我们称此为“C++三法则”。在较新的C++11标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”。也就是说,“三法则”是针对较旧的C++89标准说的,“五法则”是针对较新的C++11标准说的。为了统一称呼,后来人们干把它叫做“C++ 三/五法则”。
“需要析构函数的类也需要拷贝和赋值操作”
从“需要析构函数”可知,类中必然出现了指针类型的成员(否则不需要我们写析构函数,默认的析构函数就够了),所以,我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄漏。
那么为什么说“也需要拷贝构造函数和赋值操作”呢?原因是:类中出现了指针类型的成员,必须防止浅拷贝问题。所以需要自己书写拷贝构造函数和拷贝赋值运算符,而不能使用默认的拷贝构造函数和默认的拷贝赋值运算符。
4.2.8 类对象作为类成员
C++类中的成员可以是另一个类的对象,这种成员被称为对象成员
class A{};
class B{
A a;
};
B类中有对象A作为成员,A被称作对象成员
当其它类对象A作为类B的成员时,先构造A再构造B,析构时先析构B再析构A
4.2.9 静态成员
静态成员是在成员变量和成员函数前加上关键词static,称为静态成员。
静态成员分为:
-
静态成员变量
class A(){ static void func(){} static int a; }; int A::a = 10; //::被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。-
所有对象共享同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化(必须有初始值才能被使用)
初始化语法:
type class::name = value;,必须要初始化,但可以不赋值,不赋值的情况下默认初始化为0全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。
静态成员变量也是有访问权限的,虽然都可以在类外初始化,但类外访问不到私有静态成员变量
-
-
静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量,不能访问非静态成员变量,也不能调用非静态成员函数
静态成员函数也具有访问权限,在类外无法访问私有静态成员函数
静态成员变量和静态成员函数不属于某个对象,因为所有对象共享一份数据/函数,因此静态成员有两种访问方式:
-
通过对象访问
A ab; ab.a; ab.func(); -
通过类名访问
A::a; A::func();
即使没有实例化对象,也可以通过类来直接访问静态成员。
4.2.10 转换构造函数
C++ 允许我们自定义类型转换规则,用户可以将其它类型转换为当前类类型,也可以将当前类类型转换为其它类型。这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于类。
将其它类型转换为当前类类型需要借助转换构造函数(Conversion constructor)。转换构造函数也是一种构造函数,它遵循构造函数的一般规则。转换构造函数只有一个参数。
//1.拷贝构造函数用于类型转换
Complex(double real): m_real(real), m_imag(0.0){ } //转换构造函数,将传入的double类型的real转换成Complex类
//2.转换构造函数还可以用来初始化对象
Complex c1(26.4); //创建具名对象
Complex c2 = 240.3; //以拷贝的方式初始化对象
Complex(15.9); //创建匿名对象
c1 = Complex(46.9); //创建一个匿名对象并将它赋值给 c1
Complex(): m_real(0.0), m_imag(0.0){} //默认构造
Complex(double real, double imag): m_real(real), m_imag(imag){} //有参构造
Complex(double real): m_real(real), m_imag(0.0){}//转换构造
//上述三种构造函数其实可以合并为一个:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){}
4.2.11 类型转换函数
通过转换构造函数可以将其他类型转换为当前类类型,但是不能将当前类类型转换为其他类型,为此需要使用类型转换函数(Type consversion function),它只能以成员函数的形式出现,也就是只能出现在类中。
语法:operator type(){return data;}
operator是关键字,type是要转换的目标类型,data是type类型的数据;此函数看起来似乎没有返回值类型,但是通过type和data隐式表明了返回值类型。
-
type可以是内置类型、类类型以及由typedef定义的类型别名,任何可作为函数返回类型的类型(void除外)都能够被支持。一般而言,不允许转换为数组或函数类型,转换为指针类型或引用类型是可以的。 -
类型转换函数一般不会更改被转换的对象,通常被定义为
const成员。 -
类型转换函数可以被继承,可以是虚函数。
-
一个类虽然可以有多个类型转换函数(类似于函数重载),但是如果多个类型转换函数要转换的目标类型本身又可以相互转换(类型相近),那么有时候就会产生二义性。
4.3 C++模型和this指针
4.3.1 成员变量和成员函数分开存储
C++中类的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上(存储在类的对象所属的地址中)。静态数据成员和静态成员函数是类的一部分,而不是对象的一部分。
类
非静态成员变量
非静态成员函数
静态成员变量
静态成员函数
对象
非静态成员变量(每个对象都有各自的变量)
非静态成员函数(分开存储)
一个对象的空间=所有成员变量的大小
若类为空,则编译器会为空对象分配1字节空间,用以区分空对象占内存的位置;如果这个对象的类有虚函数的话,还可能多一个指向虚表的指针
所有函数存放在独立于对象的存储空间内
对象调用函数时,对静态成员函数直接调用不存在问题,对成员函数需要把自己以this指针传给函数以指明以哪个对象调用
所以用未初始化的指针调用静态成员函数、或者调用未使用任何成员变量的成员函数(即未用到this指针)在理论上是可行的。
4.3.2 this指针
this指针指向被调用的成员函数所属的对象,是隐含在每一个非静态成员函数内的一种指针,不需要定义,直接使用即可。
this指针的用途:
- 当形参和成员变量同名时(如有参构造函数),可以用this指针加以区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
this 是一个指针,要用->来访问成员变量或成员函数。
注意:
- this 是 指针常量,它的指向是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
- this只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
- 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用
this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this,不过this这个形参是隐式的,并不会出现在代码中,而是在编译阶段由编译器添加到参数列表中。
4.3.3 空指针访问成员函数
C++中空指针也可以调用成员函数,但需要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。
4.3.4 const修饰成员函数
const可以修饰成员变量,此时只能通过初始化列表进行初始化赋值;
同时const也支持修饰成员函数和对象。
常函数:
-
后加const的成员函数称作常函数
语法:
返回值类型 成员函数名()const{}在成员函数后加const,修饰的是this指针(相当于定义当前对象的this指针为常指针常量),让this指针指向的值不可以修改
-
常函数内不可以修改成员属性
-
成员属性声明时加关键字
mutable后,在常函数中仍然可以修改语法:
mutable 数据类型 成员属性名
常对象:
-
声明对象前加const称该对象为常对象
语法:
const 类名 对象名; -
常对象的成员属性一般不可以修改,但是可以修改声明时使用
mutable修饰的变量值或静态变量的值 -
常对象只能调用常函数,因为常函数本身保证了属性不可修改
4.4 友元
若想让类A外一些特殊的函数或者其他类访问到类内的私有属性,需要用到友元
友元的目的:让一个函数或者类访问另一个类中的私有成员
注意:**友元关系不能传递。**若A是B的友元,B是C的友元,A不一定是C的友元。
友元的关键字:firend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要加friend关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元函数可以访问当前类中的所有成员,包括public/protected/private属性。
class A{
public:
friend void GlobalFunc();
};
void GlobalFunc(){}
4.4.2 类做友元(友元类)
友元类中的所有成员函数都是另外一个类的友元函数。
例如将类B声明为类A的友元类,那么类B中所有的成员函数都是类A的友元函数,可以访问A的所有成员,包括public/protected/private
class A{
public:
friend class B;
};
class B{};
- 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
4.4.3 成员函数做友元
class B; //提前声明B
class A{
public:
friend void B::m_func();
private:
int a;
};
class B{
public:
void m_func(){}
};
一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
使用此种方法,需要注意类和成员方法的编译顺序,类的提前声明的使用范围是有限的,只有在正式声明一个类以后才能用它去创建对象。
创建对象时要为对象分配内存,在正式声明类之前,编译器无法确定应该为对象分配多大的内存,只有在类正式声明后,才能确定应该为对象预留多大的内存。在对一个类作了提前声明后,可以用该类的名字去定义指向该类型对象的指针变量或引用变量,因为指针变量和引用变量本身的大小是固定的,与它所指向的数据的大小无关。
4.5 运算符重载
对已有的运算符进行重新定义,赋予其另一种功能以适应不同的数据类型。
运算符重载也可以发生函数重载,但是对于内置数据类型的表达式运算符是不能发生重载的。
需注意,我们在很多类定义的时候都需要将成员变量定义为私有,因此使用全局函数重载运算符的时候需要将该函数定义为友元。
4.5.1 加号运算符重载(数学运算符)
四则运算符:+、-、、/、+=、-=、=、/=
关系运算符:>、<、<=、>=、==、!=
实现两个自定义数据类型(如结构和类)之间的数学运算
//成员函数实现
class Person{
public:
Person operator+(Person &p){
Person temp;
temp.m_A = this -> m_A + p.m_A;
temp.m_B = this -> m_B + p.m_B;
return temp; //temp为局部变量,不可以使用引用返回
}
int m_A;
int m_B;
};
//全局函数实现
Person operator+(Person &a, Person &b){
Person temp;
temp.m_A = p1.m_A + p1.m_B;
temp.m_B = pa.m_A + p2.m_B;
return temp;
}
调用方法:
//1.本质调用
Person p3 = p1.operator+(p2); //成员函数调用
Person p3 = operator+(p1, p2); //全局函数调用
//2.简化调用
Person p3 = p1 + p2; //编译器会自动转换成本质调用的方式
用成员函数还是全局函数重载:
执行Complex c2 = c1 + 15.6;编译器检测到+号左边(+号具有左结合性,所以先检测左边)是一个 complex 对象,就会调用成员函数operator+()。检测到15.6是一个double,于是自动调用Complex(double real)这个转换构造函数。最后执行成功。
但是如果执行Complex c3 = 15.6 + c1;就会发生错误了,因为 double 类型并没有以成员函数的形式重载 +。
也就是说,以成员函数的形式重载 +,只能计算c1 + 15.6,不能计算15.6 + c1。
- 建议以全局函数重载+、-、*、/、==、!=这类左右数据类型可颠倒的运算符
- +=、-=、*=、/=这类符号不存在数据类型颠倒的情况,首选成员函数重载
- C++ 规定,箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载
4.5.2 左移运算符重载(<<和>>)
左移运算符<<重载可以输出自定义数据类型。
若使用成员函数重载左移运算符
void operator<<(?){}
思考参数,首先成员函数参数不可以传入类实例,否则其本质形式为p1.operator<<(p2),对单个类的输出来说是不对的;其次,左移运算符的适用形式为
cout<<p,左右分别为cout和p,因此考虑将cout作为参数。但p.operator<<(cout)的形式简化后为p<<cout,cout为右值。
因此通常不会使用成员函数方法来重载<<运算符。
使用全局函数方法重载<<运算符
void operator<<(cout, Person &p){}思考参数,cout实际会报错,我们查看其定义可以发现**
cout是ostream(输出流对象)返回的对象(对应地,cin是istream返回的对象)**,并且查阅资料可知,**所有流对象都不能拷贝,因为流对象中含有指向IO缓冲区的指针,假如流对象可以复制,那么将会有两个指针同时操作缓冲区,如何释放、如何修改都会有冲突同步问题,因此流对象无法复制。**于是我们可以使用引用传递的方法来传入cout参数。
void operator<<(ostream &cout, Person &p){}此处的cout其实可以换成任意别称(引用传递嘛毕竟是,到这里其实就已经可以写函数内容并运行了,但是实际使用起来发现cout<<p;是没有问题的,但cout<<p<<endl;会报endl的错,我们可以将其看作一个链式编程,为了能连续读取新的参数,我们需要保证每个左移运算符左面都是ostream &cout类型的内容,因此我们只需要设置返回值的类型为ostream &cout即可。
于是有了最后的全局函数重载左移操作符的函数形式:
ostream &operator<<(ostream &out, Person &p){
cout<<p.m_a<<p.m_b;
return cout;
}
cout<<p<<endl; //operator<<(operator<<(cout, p),endl)
4.5.3 递增运算符重载(++/–)
使用成员函数的方式对这种运算符进行重载
需要注意的是,由于后置递增的特性,我们使用临时变量记录其初始值并返回,在这时不能返回引用类型;
这就要求在重载左移运算符
<<时的第二个参数传入的不能是对类对象的引用,C++中产生的临时对象是不可修改的,即默认为const的,非const引用只能绑定到与该引用同类型的对象,但是非常量对象可以绑定到const引用上, 因此可以去掉<<重载参数的类的引用符,或者加上const变为常量引用, 即std::ostream & operator << (std::ostream &out, const myInteger &myclass)
4.5.4 赋值运算符重载(=)
C++编译器至少给一个类添加四个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性值进行拷贝
- 赋值运算符operator=,对属性值进行拷贝
如果类中有属性指向堆区,做赋值操作时也会出现浅拷贝的问题,因此需要对赋值运算符进行重载
4.5.5 函数调用符()重载
函数调用符()也可以重载,由于重载后使用的方式非常像函数的调用,因此称为仿函数,仿函数没有固定的写法,非常灵活。重载 () 并不是创造了一种新的调用函数的方式,而是创建了一个可以传递任意数目参数的运算符函数。
语法:返回值类型 operator()(参数列表){ 函数主体 }
4.6 继承
继承是面向对象三大特性之一
在定义一些类的时候我们会发现,有些类之间是相互包含的关系,比如
动物->猫->英短、布偶
下级的成员拥有上级的共性外还拥有自己的特性,这时可以使用继承技术来减少重复代码。
4.6.1 继承的语法
class A:public B;
//class 子类 : 继承方式 父类;
A类称作子类或派生类,B类称为父类或基类
派生类中的成员包含两部分,一部分是从基类继承过来的,一部分是自己增加的成员;从基类继承过来的表现其共性,新增的成员表现了其个性。
4.6.2 继承方式
- 公共继承:可以以原本的权限访问父类中的公有和保护成员(父public->子public,父protected->子protected)
- 保护继承:可以以保护权限访问父类中的共有和保护成员(父public->子protected,父protected->子protected)
- 私有继承:可以以私有权限访问父类中的共有和保护成员(父public->子private,父protected->子private)
三种属性能力的强弱:public<protected<private,各继承方式相当于先将父类继承的所有成员都放在子类的对应部分,再根据属性强弱决定成员的属性在子类中的权限;
在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。

使用 using 可以改变基类成员在派生类中的访问权限。但 using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。
public:
using Base::m_Protected; //将从基类中继承的变量的权限从保护权限改为公共权限
4.6.3 继承中的对象模型
从父类继承过来的成员,有哪些属于子类对象中?
**不管何种继承方式,子类会继承父类中除构造函数和析构函数之外的所有成员。父类的private成员虽然可以被子类继承,但编译器会将其隐藏,子类中的任何成员方法都不能在其函数体中访问这些从父类中继承而来的private成员。**在子类中访问父类private 成员的唯一方法就是借助从父类所继承的非 private 成员函数,如果父类没有非 private 成员函数,那么该成员在子类中将无法访问。
在VS开发人员命令提示符中查看单个类的内容:
cl /d1 reportSingleClassLayout查看的类名 所属的文件名
4.6.4 继承中构造和析构的顺序
子类继承父类之后,当创建子类对象,也会调用父类的构造函数。
创建子类时,首先调用父类的构造函数,然后调用子类的构造函数;销毁子类时,先调用子类的析构函数,再调用父类的构造函数。
4.6.5 继承同名成员处理方式
当子类和父类出现同名的成员:
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名的成员函数;如果想要访问到父类中被隐藏的同名成员函数,加作用域
4.6.6 继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致。
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
//静态成员有两种访问方式,一种是通过对象访问,另一种是通过类名直接访问
//比较需要注意的是通过子类类名访问父类中同名成员变量
Son::Base::m_A;
//第一个::代表通过类名的方式访问,第二个::代表访问父类的作用域下
4.6.7 多继承语法
C++允许一个类继承多个类
语法:class 子类 : 继承方式 父类1, 继承方式 父类2…
多继承可能会引发父类中有同名成员出现,需要加作用域以区分;C++在实际开发中不建议使用多继承
4.6.8 菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承了两个派生类,这种继承被称为菱形继承或钻石继承
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。利用虚继承可以解决这种问题。在继承之前加上关键字virtual变为虚继承,使得派生类中只保留一份间接基类的成员。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。
虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。(隔代遗传)
在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。
4.7 多态
4.7.1 多态的概念
派生类对象的地址可以赋值给基类指针。对于通过基类指针调用派生类和基类中都有的同名、同参数列表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类的对象,则派生类的函数被调用(当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定)。这种机制就是多态(polymorphism)。
虚函数:声明时前面加了 virtual 关键字的成员函数。virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。静态成员函数不能是虚函数。包含虚函数的类称为多态类。
**一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,即使没有virtual关键字,该函数均保持虚函数的特性。
多态分两类:
- 静态多态:函数重载和运算符重载都属于静态多态
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定- 运行阶段确定函数地址
多态满足条件:
- 有继承关系
- 子类重写(函数返回值类型、函数名、参数列表完全一致)父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
class A{void func_a; void func_b(); int var;};
class B{void func_a; virtual void func_b(); int var;};
以在堆区创建的对象为例:

sizeof(B)会比sizeof(A)大4个字节,实际上是所出来的虚函数表指针(vptr),它指向虚函数表(vtbl),表中的数据是函数指针,储存了fun_b()具体实现对应的位置。虚函数实现的过程是:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。
构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。
通过基类的引用调用基类和派生类中同名、同参数表的虚函数时,若其引用的是一个基类的对象,则被调用是基类的虚函数;若其引用的是一个派生类的对象,则被调用的是派生类的虚函数。
4.7.2 纯虚函数和抽象类
在多态中,父类中虚函数的实现一般是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有纯虚函数存在时,这个类也被称为抽象类
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
4.7.3 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在使用时无法调用到子类的析构函数,因此需要将父类中的析构函数改为虚析构或纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现,在此纯虚析构与纯虚函数相区别,纯虚函数不含实现,而纯虚析构需要在类内声明后在类外实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,则该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0;
- 虚析构和纯虚析构都是为了解决通过父类指针释放子类对象的问题
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构的函数也属于抽象类
5 文件操作
C++中对文件操作需要包含头文件
文件类型:
- 文本文件 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
文件操作的三大类:
- ofstream 写操作
- ifstream 读操作
- fstream 读写操作
5.1 文本文件
5.1.1 写文件
步骤:
- 包含头文件
#include - 创建流对象
ofstream ofs; - 打开文件
ofs.open(“文件路径”,打开方式); - 写数据
ofs<<“写入的数据”; - 关闭文件
ofs.close();
| 打开方式 | 解释 |
|---|---|
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 若文件存在,先将其删除再创建 |
| ios::binary | 二进制方式 |
文件打开方式可以混用,利用 | 操作符。e.g : ios::binary | ios::out
5.1.2 读文件
判断文件是否打开成功:
//使用is_open()成员函数
if( !ifs.is_open() ){
cout<<"打开失败"<<endl;
}
步骤:
- 包含头文件
#include - 创建流对象
ifstream ifs; - 打开文件
ifs.open(“文件路径”,打开方式); - 写数据 四种方式读取
- 关闭文件
ifs.close();
//文件读取方式
//1 流输入方式读取,遇到空格或换行符结束当前读取
char buf[1024] = {0};
while( ifs >> buf ){ //
cout<<buf<<endl;
}
//2 getline()作为istream的成员函数,一次读取整行数据,遇到换行符结束当前读取;两形参的作用是从istream中读取至多sizeof(buf)的字符保存在buf数组中;即使没有那么多字符,读到换行符时也会终止当前读取
char buf[1024] = {0};
while(ifs.getline(buf, sizeof(buf))){ .. }
//3 getline()全局函数,与成员函数作用相同,但是两形参分别是正在读取的输入流和接受输入字符串的string变量的名称
string buf;
while(getline(ifs,buf)){ .. }
//4 (不推荐 效率低)使用单个字符读取,get()每次获取一个字符赋值给c,EOF为end of file
char c;
while( (c = ifs.get()) != EOF ){ cout<<c; }
5.2 二进制文件
以二进制的方式对文件进行读写操作,打开方式应指定为ios::binary
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream &write(const char * buffer, int len);
参数解释:字符指针buffer指向内存中的一段存储空间,len是读写的字节数
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数 read
函数原型:istream &read(char *buffer, int len);
参数解释:字符指针buffer指向内存中的一段存储空间,len是每次读写的字节数
C++中的面向对象特性包括封装、继承和多态。封装通过访问权限控制成员变量和函数,保证数据安全;构造函数和析构函数用于对象的初始化和清理;拷贝构造函数处理对象的复制。继承允许创建类的层次结构,多态则通过虚函数实现动态绑定,允许基类指针调用派生类的成员函数。纯虚函数和抽象类用于定义接口,而静态成员和友元提供特殊访问权限。
9221

被折叠的 条评论
为什么被折叠?



