类的特性
类编程与应用编程分开
面向对象程序设计两个原则:抽象和分类
面向对象程序设计程序规模大,但程序设计效率高(因代码重用,类编程与应用编程的分工合作),程序结构易理解,编译运行的效率即产生的机器代码规模和运行时间更小,适应于大规模设计
面向对象程序设计中代码间的相关性低(低耦合特性),使得代码很容易被复用和扩展,同时也说明了面向过程的代码重用性低、扩展能力差。
封装
数据与算法(操作)结合,构成一个不可分割的整体(对象)。在这个整体中一些成员是保护的,它们被有效地屏蔽,以防外界地干扰和误操作;另一些成员是公共的,它们作为接口提供给外界使用。
继承
一种机制,自动地为一个类提供来自另一个类地操作和数据结构,使得程序员只需定义已有类中没有的成分来建立新类
多态
在运行时,能依据其类型确认调用哪个重载函数的能力,为多态性
比如说开门,机器自动判断为开冰箱门还是机舱门
函数
-
重载:对于在不同类型上做不同运算而又用同样的名字的情况
-
重载函数至少在参数个数、参数类型或参数顺序上有所不同,仅仅是返回类型有所不同不行
这种也不行
void fn(double); void fn(long); int a; fn(a);//error,有二义性
数组
-
定义数组时,数组元素个数不能为变量
int a[i];
指针
-
数组名是指针常量,不能作为左值
-
malloc分配内存时,若内存空间不足,则返回NULL
-
int *a=new int[size]
;delete[] a;
-
常量指针
const int *p;
指针值可以变,指针指向的内容不可以变 -
指针常量
char *const p="abcd";
指针值不可以变,指针指向的内容可以变 -
常量指针常量
const char *const p="abcd";
指针值和指针指向的内容都不可以变 -
指针的使用会带来一些问题
- 可读性差
- 破坏函数的黑盒特性,重用性问题
- 调试复杂问题
-
空类型指针不能赋给其他指针
-
输出字符指针就是输出字符串,输出字符指针的引用就是输出单个字符
char *ch="1234"; cout<<ch; //输出1234 cout<<*ch; //输出1
-
指针数组与二维数组
-
函数指针指向代码区中的某个函数
int (*func)(char a,char b)
或int fun()
中的fun为一个地址,是个指针常量
引用
-
引用不是值,不占存储空间
-
引用只有声明没有定义
-
引用在声明时必须被初始化,且以后不会指向其他变量
-
不能建立引用的数组
int a[10]; int & ra[10]=a //error int *& ra=a //error,a是指针常量 //下面这个就对了 int a[3]={1,2,3}; int *pa=a; int * &ra=pa;
-
引用的好处
-
帮助函数返回多个值
-
用引用返回值,不产生任何返回值的副本,如下
float temp; float &fn2(float r){ temp=r+1; return temp; } float c=fn2(5.0);
-
参数传递时不用复制副本,解决大对象的传递效率和空间使用问题
-
行使了指针的间接访问的作用,却把指针修改的操作限定在了初始化环节,避免了因修改指针引起的隐性错误
-
对操作符重载的补充?
-
-
返回引用的函数,可以使函数成为左值,只要返回的变量不是局部作用域的变量
-
传递const指针和引用,保护实参不被修改,如
double& fn(const double& p){}
-
引用的初始化可以是变量,(也可以是常量?)也可以是一定类型的堆空间变量 ,如下
int * p=new int; *p=1; int &r=*p; //int &r=1//? int &a=new int;//error
返还堆空间
delete p; //或 delete &r;
结构
- 声明结构不分配内存
- 结构体之间可以直接赋值
- 结构体作为实参时,一般用引用传值
类
-
类结尾的分号不能丢
-
类里面声明为private的变量外部不能访问,作右值也不行
-
类定义中成员默认private
-
::域区分符,可用于在成员函数里操作全局变量,也可区分同名
-
在类中定义的成员函数默认为内联函数,即使不加inline标示,因此类中定义的成员函数一般规模比较小
-
一般类定义分为两部分,类定义的头文件(.h)和类的成员函数定义(.cpp)
//********************* //** tdate.h类定义 **//类的外部接口 //********************* class Tdate{ public: void Set(int,int,int); //成员函数声明 int IsLeapYear(); void Print(); private: int month; int day; int year; }; //********************* //** tdate.cpp **//成员函数定义//类的内部实现 //********************* void Tdate::Set(int m,int d,int y) { month=m; day=d; year=y; }
-
只有同类的同名函数才能称之为重载
-
对象也有指针和引用
-
在成员函数中访问成员不需要用对象名作前缀,可以用this->数据成员,因为隐式传输对象的地址区分是哪个对象的数据成员
-
一个类对象占的内存空间=它的数据成员空间总和,与成员函数空间无关
-
类名遮挡P247
-
非类型名不能重名,类型名和非类型名可以重名但使用类时要加class(P248)
-
C++程序结构,体现了类的封装和重用(class.h ;class.cpp ;main.cpp)
构造函数
-
定义对象时分配内存空间
-
构造函数是与类同名的成员函数,没有返回类型,可以有参数,可以重载
-
调用构造函数时,先分配对象空间(即对数据成员定义),再对执行构造函数中的操作对数据成员初始化
-
析构函数没有返回类型,没有参数,没有重载,在类对象生命期结束的时候由系统自动调用
-
带参数的构造函数,无参的构造函数叫默认构造函数
class Student{ public: Student(char* pName) { cout <<"constructing student " <<pName<<endl; strncpy(name,pName,sizeof(name)); name[sizeof(name)-1]='\0'; } }; Student ss("Jenny");
-
构造函数不可以调用另一个重载的构造函数P267,重载构造函数有可能存在二义性
-
系统提供一个默认的构造函数,为无参的,用户只要自己定义构造函数会将其覆盖
-
定义一个全局或静态对象,若构造函数不对数据成员初始化,则数据成员默认初始化为0;
-
Tdata aday;不等于Tdata aday();
-
类成员的初始化:初始化放在构造函数中和类数据成员声明时都不合适,只能用冒号语法放在构造函数外。
Student(char* pName="no name",int ssID=0):id(ssID)
class StudentID{ public: StudentID(int id=0){ value=id; } protected: int value; }; class Student{ public: Student(char* pName="no name",int ssID=0):id(ssID) { ... } protected: char name[20]; StudentID id; }; void main(){ Student s("Randy",9818); Student t("Jenny");//Student t="Jenny"; }
-
常量和引用的初始化:放在构造函数正在建立数据成员结构空间的时候,也就是构造函数的冒号后面
class S{ public: S(int &i):ten(10),refI(i){} protected: const int ten; int &refI; };
-
静态对象只构造一次,即只调用一次构造函数
static Student s1(17);
-
构造数据成员的顺序要看类中声明的顺序,而不是看构造函数冒号后面类成员初始化的顺序(P280)
堆与拷贝构造函数
-
Tdata *p;
不为p调用构造函数,Tdata p;
调用构造函数 -
new可以自动调用构造函数,delete自动调用析构函数
new后面也可以跟参数
class Tdata{ public: Tdata(int m,int d,int y); protected: int month; int day; int year; }; Tdata *p; p=new Tdata(1,1,2020);//构造函数有参数,new后面也要有参数 delete p;
-
new分配对象数组
class Student{ public: Student(char *pName="no name"){} protected: char name[40]; }; Student *p=new Student[8];//后面不能跟参数,因此构造函数的形参 //必须默认或者无 delete []p;
-
涉及对象拷贝的情况
-
Student s2=s1
;Student s2(s1)
//引用不会调用拷贝构造函数如 //Student &s2=s1
-
对象作为函数参数传递(注意函数返回时形参利用拷贝构造函数创建的对象要析构)P307
void fn(Student fs);//参数一定要是对象 fn(Student(17)); void fn(Student &fs);//不会调用拷贝构造函数
-
-
默认拷贝构造函数,一个成员一个成员的拷贝,对于对象成员调用其拷贝构造函数P309(例题很重要)
-
浅拷贝:使用默认的拷贝构造函数,不拷贝资源
-
深拷贝:使用自定义的拷贝构造构造函数,不但拷贝成员,也分配和拷贝资源
Student(Student&)
- 当类需要自定义析构函数析构资源时,需要自定义拷贝构造函数实现深拷贝(堆内存,打开文件,占用设备)
-
临时对象(函数返回对象创建一个临时对象)的有效范围:在创建它们的外部表达式范围内有效,这个表达式;结束则析构对象P313
-
无名对象***?***
void fn(Student &s); Student &refs = Student("Randy");//初始化引用,多余 Student s = Student("Jenny");//初始化对象定义, //等同于Student s = "Jenny"; //不会先创建临时变量再拷贝构造 fn(Student("Danny"));//函数参数, //这样传过去不会调用拷贝构造函数 //使代码更精简 Student p(39); p=20;//首先调用构造函数创建数据成员为20的无名对象再调用赋值运算符 //最后调用析构函数将无名对象析构
静态成员与友元
-
全局变量的坏处:引起软件设计的混乱,一旦程序变大,维护量就急剧提升,破坏类的封装性,所以使用静态成员防止全局变量的使用
-
静态成员:与类相联系,只认类型,不认对象
-
静态数据成员
-
让所有对象在类的范围内共享某个数据
-
静态数据成员使用范围
- 用来保存流动变化的对象个数,如学生数目
- 作为一个标志,如一个文件是否正在被使用
- 指向链表的首指针或尾指针
-
静态数据成员不属于某一个特定的对象,可用s1.num访问,也可用Student::num访问
-
多文件结构中静态数据成员声明与初始化的位置:声明在类声明中(Student.h),定义和初始化在类的内部实现中(Student.cpp)
//Student.h class Student{ static int num; //若写成num=0;则非法 char name[40]; public: Student(char* pName ="no name"); ~Student(); static int number(); //静态成员函数 }; //Student.cpp int Student::num=0;//不能放在类的声明中 //不要放在main函数外部的全局位置,不易于重用 //一定要写,因为类中声明的不分配空间,也不默认为0 //main.cpp ...
-
-
静态成员函数
- 静态成员函数中不能对非静态成员直接访问,因为不知道是哪个对象的
- 静态成员函数与非静态成员函数的区别是,静态成员函数没有this指针
-
-
需要友元的原因
- 提高效率,普通函数直接访问类成员
- 方便重载操作符的使用
-
A类中的成员函数想访问B类的私有数据成员,可将A类中的成员函数作为B类的友元
-
友类:友类中的每个成员函数都可以访问本类的私有数据成员
class Student;//交叉声明中,Teacher中有Student对象,Student中有Teacher对象,要注意类名声明 class Teacher{ public: void assign(Student&s); protected: int num; Student *pLists[100]; }; class Student{ public: friend class Teacher;//友类 protected: //... };
继承
-
子类可以传递给父类,将继承的部分传递给父类
-
派生类若无自定义的构造函数,则调用基类的构造函数
-
派生类一般不直接访问基类的数据成员(继承的部分),而是通过基类的接口(成员函数)访问,这样保持独立性,以接口做沟通
-
派生类的构造总是由基类的初始化开始
class GraduateStudent :public Student{ public: GraduateStudent(char *Name,Advisor&adv):Student(Name),advisor(adv) //先构造基类部分,再调用Advisor的拷贝构造函数构造对象advisor { Grade=0; } protected: Advisor advisor; int Grade; }; //析构时先析构GraduateStudent再析构Advisor再析构Student
-
公有继承的派生类在派生类作用域可以访问基类的公有成员和保护成员,在类作用域之外如main函数中就不能直接访问基类的保护成员,仍然可以直接访问基类的公有成员
-
私有继承和保护继承的派生类不是子类,不能做基类能做的所有事
- 私有继承的派生类在类作用域之外(公共场合)不能直接访问基类的公有成员和保护成员,可以通过自己的成员函数间接访问基类的公共成员和保护成员
- 私有继承的派生类不可以作为函数参数传递给基类
-
要想访问到私有成员,只能通过基类的成员函数访问(即征询基类的同意)
-
**调整访问控制:**可以调整在派生类中可见的成员的访问控制,即派生类私有继承了一个原基类的公有成员 b3,变成了私有成员,在派生类作用域外不可直接访问,用
using Base::b3
,可将b3变为公有 -
多继承
class SleeperSofa:public Bed,public Sofa{};
- 多继承具有模糊性,即继承的基类具有同一属性—>名称冲突—>在具体属性前加上基类
ss.Sofa.Setweight(10);
—>复杂了—>解决方法:虚拟继承
- 多继承具有模糊性,即继承的基类具有同一属性—>名称冲突—>在具体属性前加上基类
-
虚拟继承用于多继承,为了使得继承不冗余
-
多继承的构造顺序,不管派生类的构造函数的顺序,即不管构造函数冒号后面的顺序
- 按虚拟继承的基类顺序
- 按非虚拟继承的基类顺序
- 按组合对象的声明顺序(在声明数据成员处)
- 构造函数中的顺序
多态
-
多态基础是继承和重载
-
在运行时,能依据其类型确认调用哪个重载函数的能力,为多态性,也称迟后联编
-
虚函数实现多态:只要在基类中成员函数加上virtual字段,派生类中地重载函数不需要添加virtual
-
多态,若不同名或参数不同或返回类型不同,则不能多态(P367)
-
上述有个例外,当重载函数仅返回不同类型(基类虚函数返回基类指针或引用,子类虚函数返回子类指针或子类引用)时也可实现多态
-
虚函数的限制
-
只有类的成员函数可以为虚函数
-
静态成员函数、内联函数、构造函数不可以是虚函数
-
析构函数通常定义为虚函数
void finishWithObject(Base * pHeapObject){ delete pHeapObject; }
将析构函数声明为虚函数,析构的时候自动选择调用基类或者派生类的析构函数
-
-
抽象类(继承设计问题引出的)
-
不能有实例对象的类,唯一用途为被继承
-
至少有一个纯虚函数,纯虚函数不能实现,用于被重载
纯虚函数是一个没有定义函数语句的虚函数,是在基类中为子类保留的一个位置,以便子类用重载函数覆盖之
class Account{ public: Account(){} virtual void Withdrawal(float amount) = 0;//纯虚函数 private: int a; };
-
-
如何判断抽象类与具体类
所有纯虚函数被重载之前,抽象类的子类一直都是抽象类,即若抽象类的子类未将基类中所有纯虚函数重载,则此子类还是抽象类
-
可以声明一个抽象类的指针或引用,用于函数形参,实现多态
运算符重载
-
目的:使代码更直观更易读,将运算符的定义扩展到操作数为对象的情况
-
operator 运算符( )中的参数常用引用传递,不用指针,为了可读性P392
-
运算符返回可以值返回也可以引用返回,operator +()返回值,operator ++()返回引用
-
运算符使用场景
-
作为成员函数,可比作为普通函数说明时少一个参数
Num operator +(Num & n){}
-
作为普通函数,在类中添加此运算符的友元,这样就可以访问类中的保护成员(注意参数不能全为内部类型)
Num operator +(Num &n1,Num&n2){}
-
-
C=C+2.5或C=2.5+C这种对象和内部类型的运算
-
定义两个重载运算符,参数颠倒
-
定义一个转换构造函数,类似类型转换,将内部类型转换为对象类型再相加,减少了运算符的定义
c=c+Num(1.5);//显示转换,构造一个无名对象 c=c+1.5;//若定义了转换构造函数,系统自动寻找可能的转换
-
-
=、()、[ ]、->必须为成员形式
有时成员形式不能解决问题,如
c=1.5+c;
不能用operator +(Num& c)
匹配,只能用非成员形式operator +(double d,Num&c)
匹配 -
增量运算符的重载(注意一个返回引用一个返回值)
Increase & Increase::operator++()//前增量,返回引用 { value++; //先增量 return *this; //再返回原对象 } Increase Increase::operator++(int)//后增量,返回值,参数int与前增量区别 { Increase temp(*this); //临时对象存放原有对象值 value++; //原有对象增量修改 return temp; //返回原有对象值 }
-
转换运算符(是一个简化c=c+1.5的方法,不用重载
+
运算符,还可直接计算c=c1+c2)(c为对象)-
例子
operator double(){}
class RMB{ public: RMB(double value = 0.0); operator double()//无返回类型,将对象转换为double类型 { return yuan + jf / 100.0; } void display(){ cout << (yuan + jf / 100.0) << endl; } protected: unsigned int yuan; unsigned int jf; }; void main() { RMB d1(2.0), d2(1.5), d3; d3 = RMB((double)d1 + (double)d2); //显示转换 d3 = d1 + d2; //隐式转换,将对象转换为double类型再 //用double类型的+,运算的结果隐式转换为临时变量传给d3 d3.display(); }
-
转换运算符与转换构造函数互逆
-
-
赋值运算符
-
区分赋值运算符和拷贝构造函数
Student s=s1;//拷贝构造函数 s=s2; //赋值运算符,C++语言天然支持同一对象之间赋值, //但是当遇到深拷贝(数据成员为指向堆空间的指针)时,就需要重载赋值运算符
-
重载赋值运算符
Name & operator =(Name & s)
Name & operator =(Name & s) { deleteName();//取消已有资源 copyName(s.pName);//分配新资源 return *this; }
- 返回引用,作为左值 (a=b)++,最后a要++
- 分为两部分,取消已有资源,分配新资源
-
I/O流
-
printf和scanf
的缺陷- 不可扩充性,无法输出类对象
- 编译检查不出错误,浪费程序员时间
-
I/O标准流类
istream ostream
ostream cout(stdout);//cout为ostream类的一个对象,参数为屏幕
-
文件流类
ofstream ifstream fstream
ifstream fin("abc.txt"); char buffer[80]; fin>>buffer;//将文件中的数据输入到字符数组中
-
C字串流类
ostrstream istrstream strstream
-
流对象有成员函数如
cout.precision(5) cin.getlin(str,sizeof(str),'X') cin.get()
-
重载插入运算符
- 插入运算符不能为成员函数,不能为虚函数
- 要实现多态的运算符,只能通过间接实现
void RMB::display(ostream &out){//参数为ostream 灵活 out<<yuan<<...; } ostream& operator <<(ostream&oo.RMB& d){//返回类型为引用,参数为ostream,能处理各种输出不 //一定输出到屏幕 d.display(oo);//间接处理RMB中的保护成员,因此插入运算符不必为友元,为了用display的多态实现 //插入运算符的多态 return oo;//返回ostream类型而不是cout,为了实现更多情况的输出 }
模板
-
使程序员快速建立具有类型安全的类库集合和函数集合,方便了更大规模的软件开发,代码重用
-
函数模板与模板函数(调用函数时创建模板函数)
- 函数模板避免了相同操作的重载函数定义
template <class T> T max(T a,T b){ return a>b?a:b; } cout<<max(3,5)<<max(3.4,5.6); /*编译发现用指定类型调用函数模板时,创建模板函数: int max(int a,int b) float max(float a,float b) { { return a>b?a:b; return a>b?a:b; } } /*
- 可以重载模板函数,覆盖模板
-
类模板与模板类(创建对象时创建模板类的定义)
template<class T> class List{//类模板 public: List(); void Add(T&); ~List(); protected: Node *pFirst; //链首结点指针 }; template<class T> List<T>::List()//成员函数模板 { pFirst = 0; } template<class T>//成员函数模板 void List<T>::Add(T& t) { } List<float> floatlist;//编译发现正在创建一个模板类的对象时,根据类模板生成模板类的定义,同时创 //建对象实体
-
类模板和函数模板一般放在头文件中,因为创建对象时要生成模板类的全部定义,包括成员函数,所以类模板的成员函数定义也要放在头文件中,而不是放在cpp文件中
异常处理
-
概念:对所能预料的运行错误进行处理的一套实现机制
-
目的:在异常发生时,尽可能减小破坏,周密地善后,而不去影响其他部分程序的执行
-
try中定义可能产生错误的语句;catch中定义异常处理的语句;
-
try与catch必须连在一起,,throw与catch可以不在一个函数中
-
执行catch后的异常处理程序后,try中剩余语句不再执行,执行catch后的代码
-
catch规则
- 只有一个形参,形参可为对象类型
- 形参名可以不写
- 跟在try语句后面,可有一个或多个
- throw的参数类型必须与catch的形参类型严格匹配,才能执行异常处理程序,否则执行系统默认abort();
- catch(…)与任何throw都匹配
- catch(基类)和catch(…)放在最后
-
抛出异常的代码区域必须是用户定义的异常区域,如throw语句块内以及throw调用的函数内,否则即使抛出的数与catch实参匹配,也只能被abort()捕获