C++开发入门到精通(享精品公开课)
http://study.163.com/course/courseMain.htm?courseId=1002907007
海同网校 柴强
课时2 HelloWorld与命名空间
课时3 引用和引用参数
引用和指针不同
课时4 内联函数
限制:
内联函数不能含有复杂的结构控制语句,如switch和while,否则视同普通函数。
递归函数不行
限制在小型,频繁调用的函数。
内联函数与宏定义:
宏定义可以代替小函数定义,但是有缺陷
宏定义不检查参数类型
往往造成实际结果不能代表程序员的意图
宏的作用可以用内联函数代替
通过一个内联函数可以得到所有宏的替换功能和所有可预见的状态以及常规函数的类型检查。
默认参数的函数
为可以不指定的参数提供默认值
默认参数的顺序规定:
如果一个函数有多个默认参数,则形参分布中,默认参数应从右到左逐渐定义。当调用函数时,只能向左匹配参数。
课时6 函数重载
两个以上的函数,取相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是函数的重载。
匹配重载函数的顺序:
寻找一个严格匹配
通过内部转换寻求一个匹配,只要找到,就用那个函数
通过用户定义的转换寻求一个匹配,若查出有唯一的一组转换,就用那个函数
编译器不以形参名和返回值识别重载
默认参数和函数重载:
默认参数可以将一系列简单的重载函数合成为一个
有时候可以用默认参数代替
如果一组重载函数(可能带有默认参数)都允许相同实参个数的调用,则会引起调用的二义性。
在C语言头文件中的 extern "C"
#ifndef _FUNC_H_
#define _FUNC_H_
#ifdef __cplusplus
extern "C" {
#endif
int foo(char, int);
#ifdef __cplusplus
}
#endif
#endif
例子:
g++ -c main.cpp -o main.o
gcc -Wall -g -c fun.c -o fun.o
g++ main.o fun.o -o test
如果fun.h里没有使用extern "C" {} 那么
objdump -t main.o 显示符号表里foo函数名是_Z3fooci
objdump -t fun.o 显示符号表里foo函数名是foo
链接会出错
使用extern "C" {}之后,main.o里的符号表已经把foo函数名按c语言的规则定义
课时7 函数模板
泛型编程:独立于任何特定类型的方式编写代码。
模板是泛型编程的基础
函数模板的一般定义形式:
template<类型形式参数表> 返回类型 FunctionName(形式参数表)
{
//函数定义体
}
对于具有各种参数类型,相同个数,相同顺序的同一函数(重载函数),如果用用宏定义来写,不能检查其数据类型,损害了类型安全性。
章节3 类、对象和封装
课时8 类和对象-1
结构化程序设计
程序=算法 + 数据结构
算法第一,数据结构第二
面向对象程序设计OOP
object oriented programming
程序=对象+对象+...
关键:让每一个对象负责执行一组相关任务
面向对象编程开发范式的特性
万物皆对象
程序是一组对象彼此之间在发送消息
每个对象都有自己的内存占用,可以组装成更大的对象
每个对象都有类型,特定类型的所有对象可以接收相同的消息
概念
类
类是创建对象的模板和蓝图
类是一组类似对象的共同抽象定义
对象
对象是类的实例化结果
对象是实实在在的存在,代表现实世界的某一事物
对象的三大关键特性
行为:对象能干什么
状态:对象的属性,行为的结果
标识:对象的唯一身份
区别
类是静态定义
对象是动态实例
程序代码操作的是对象而非类
建模得到的是类而非对象
联系
类是对象的定义
对象的产生离不开类这个模板
类存在的目的是实例化得到对象
世界由对象组成
定义一个类的步骤
定义类名
编写类的数据成员代表属性
编写类的方法代表行为
类的建模是一个抽象和封装的过程
抽象:去掉不关注的、次要的信息而保留重要的信息
封装:信息打包
具体一点:将数据和行为结合再一个包中,对对象的使用者隐藏数据的实现方式
实现封装的关键:不能让类中的方法直接访问其他类的内部数据,只能通过公开行为方法间接访问。
两个关键字
struct 安全性不好,任何人都可以访问
class 类不仅可以保护数据,还可以提供成员函数操作数据
C++用类来定义抽象数据类型(Abstract Data Type)
C++早期版本被称为带类的C(C with classes)
例子:
class 类名称{
public:
//公有函数
protected:
//保护成员
private:
//私有函数
//私有数据成员
int val;//数据成员
}
课时9 类和对象-2
在类中定义成员函数
类中定义的成员函数一般为内联函数,即使没有明确用inline标识
在C++中,类定义通常在头文件中,因此这些函数定义也伴随着进入头文件
在类之后定义成员函数
C++允许在其它地方定义成员函数
将类定义和其成员函数定义分开,是目前开发程序的通常做法
我们把类定义(头文件)看成是类的外部接口,类的成员函数定义看成是类的内部实现。
函数不占用对象内存
类成员函数的重载
类的成员函数可以像普通函数一样重载
但不同类即使有相同的函数名也不算重载
类成员函数的默认参数
类的成员函数可以像普通函数一样设置默认参数
在类声明外定义成员函数
void Tdata::set(int m, int d, int y) {mouth=m;}
g++ -o test car.h car.cpp main.cpp
调用成员函数
必须指定对象和成员名称
在成员函数中访问成员
成员函数必须用对象来调用
在成员函数内部,访问数据成员或成员函数无须如此
this指针代表当前对象占用内存空间的地址
用指针来调用成员函数
用引用传递来访问成员函数
用对象的引用来调用成员函数,看上去和使用对象自身的简单情况是一样的
指针 Car* pcar 使用 pcar->print()
引用 Car& rcar 使用 rcar.print()
课时10 封装
OOP三大特性
封装 encapsulation
继承 inheritance
多态 polymorpism
类背后隐藏的思想是数据抽象和封装
信息隐藏,隐藏对象的实现细节,不让外部直接访问到
将数据成员和成员函数一起包装到一个单元中,单元以类的形式实现。
将数据成员和成员函数包装进类中,加上具体实现的隐藏,共同被称作封装,其结果是一个同时带有特征和行为的数据类型
定义类,定义其数据成员、成员函数的过程称为封装类
信息隐藏是OOP最重要的功能之一,也是使用房屋修饰符的原因
信息隐藏的原因包括:
对模块的任何实现细节所作的更改不会影响使用该模块的代码
防止用户意外修改数据
使模块易于使用和维护
访问修饰符:private protected public
除非必须公开底层实现细节,否则应该将所有字段指定为private加以封装
使数据成员私有,控制数据访问限制,增强了类的可维护性
隐藏方法实现细节(方法体),向外部提供公开接口(方法头),以供安全调用
简化调用,方便修改维护
程序开发人员按角色分为类创建者和客户端(应用)程序员
客户端程序员目标:收集各种用来实现应用开发的类
类创建者目标:构建类,向应用程序员暴露必须的部分,隐藏其他部分
在任何相互关系中,具有关系所涉及的各方都遵守的边界,创建类就建立了与客户端(应用)程序员之间的关系
封装及访问控制首要存在原因:让客户端(应用)程序员无法触及他们不应该触及的部分--隐藏细节
封装及访问控制第二个原因:允许类创建者改变类内部工作方式而不用担心会影响到客户端(应用)程序员--隔离变化,利于维护
课时11 构造函数与析构函数-1
编码规范:声明变量赋初值
int l = 0;
int *p = NULL; C++11使用空指针关键字 nullptr
malloc申请到的内存区域使用memset进行设置
对象在定义时进行初始化
类的对象的初始化只能由类的成员函数来进行
建立对象的同时,自动调用构造函数
类对象的定义涉及到一个类名和一个对象名
由于类的唯一性和对象的多样性,用类名而不是对象名来作为构造函数名是比较合适的
默认构造函数
构造函数负责对象初始化工作,将数据成员初始化
创建对象时,其类的构造函数确保:在用户操作对象之前,系统保证初始化的进行
建立对象,须有一个有意义的初始值
C++建立和初始化对象的过程专门由该类的构造函数来完成
构造函数给对象分配空间和初始化
如果一个类设有专门定义构造函数,那么C++就仅仅创建对象而不做任何初始化
构造方法满足一下语法规则:
1 构造方法名与类名相同
2 没有返回类型
3 方法实现主要为字段赋初值
放在外部定义的构造函数,其函数名前要加上“类名::”
构造函数另一个特殊之处是它没有返回类型,函数体重也不允许返回值,但可以有无值返回语句“return;”
因为构造函数专门用于创建对象和为其初始化,所以它是在定义对象时自动调用的
如果建立一个对象数组
Desk dd[5]; //对象数组dd,构造函数会被调用5次
12 构造函数和析构函数-2
构造函数的初始化列表
构造函数的初始化列表中数据的初始化顺序和声明的顺序相同
class Student
{
public:
Student(int sslD=0):id(sslD),score(100) //冒号表示后面要对类的数据成员进行初始化
{
cout<<sslD<<endl;
}
private:
int id;
int score;
}
构造函数可以有多个
构造函数可以重载
构造函数用于隐式类型转换
调用fun(1002)等同于Student john(1002);fun(john);
explicit Student(int id=0);
添加explicit关键字,可以静止隐式类型转换
如果一个类没有定义构造函数,C++编译器将提供一个默认的构造函数
析构函数
- 一个类可能在构造函数里分配资源,这些资源需要在对象不复存在以前被释放
- 析构函数也是特殊类型的成员,它没有返回类型,没有参数,不能随意调用,也没有重载。只是在类对象生命期结束的时候,由系统自动调用。
- 析构函数名,就在构造函数前加上一个逻辑非运算行“~”,表示“逆构造函数”
- 如果类没有自定义析构函数,则编译器提供一个默认的析构函数
- 析构函数只有一个,不能重载
课时13 标准库类型string
- C++提供的抽象数据类型(Abstract Data Type),用于进行字符串操作
- 字符串是软件系统中最常见的数据结构之一
- 中文的字符编码标准为GB18030,一般一个汉字占用的内存为两个字符
- 编写string类是常见的笔试题目
- string类型支持可变长度的字符串
- C++标准库负责管理与存储字符串所占用的内存
#include<string>
using namespace::std;
- 字符串对象的初始化方法
初始化方法 代码解释
string s1; 默认构造函数,s1为空字符串
string s2(s1); 将s2初始化为s1的一个副本
string s3("value"); 将s3初始化为字符串的副本
string s4(n,'c'); 将字符串初始化为字符c的n个副本
string操作 代码解释
s.emtpy() 如果字符串为空,返回true,否则返回false
s.size() 返回字符串中字符的个数
s[n] 返回字符串中的第n个字符,下表从0开始
s1+s2 将s1和s2连接成为一个新的字符串,返回新生成的字符串
s1=s2 将s1的内容替换为s2的内容
v1==v2 比较v1和v2的内容,相等则返回true,否则返回false
!=,<,<=,>,>= 保持这些操作符惯有的含义
课时14 static类成员
- 假设我们使用C++开发一款游戏,有如下场景
- 假设有一个具有火星人和其他人物的视频游戏。每个火星人都很勇敢,而且当火星人注意到至少存在五个火星人时,它总是袭击其他空间的生物。如果少于五个人,则每个火星人将非常胆小。所以,每个火星人都需要知道现在游戏中有多少个火星人。
- 使用全局变量?
- 会增加系统耦合度
- 每个对象都维护一个count?
- 占用内存增加,不合理
- C++提供static数据成员
- martainCount是所有火星人类共有的
class Martain
{
public:
Martain();
~Martain();
void fight();
private:
static martainCount; //定义类的静态成员
}
- 在static成员函数中不能使用this指针
- 即使没有实例化类的对象,static数据成员和成员函数仍然可以使用
- static成员的名字在类的作用域中,因此可以避免与其他类的成员或者全局对象名字冲突
- 可以实施封装,static成员可以是私有成员,而全局对象不可以
- 通过阅读代码容易看出static成员是与特定类关联的,清晰的显示程序员的意图
课时15 动态内存分配-1
- C语言的动态分配
- malloc/free函数
- 内存区域
+-----------+
| data area | 全局变量、静态数据、常量
+-----------+
| code area | 所有类成员函数和非成员函数代码
+-----------+
| stack area | 为运行函数而分配的局部变量、函数参数、返回数据、返回地址等
+-----------+
| heap area | 动态内存分配区
+-----------+
- C++的运算符new/delete
- 在堆上生成对象,需要自动调用构造函数
- 在堆上生成的对象,在释放时需要自动调用析构函数
- new/delete,malloc/free需要配对使用
- new[]/detele[]生成和释放数组
- new/delete是运算符,malloc/free是函数调用
以上运算符要配套使用,否则会出错。
Test* pVal = new Test();
delete pVal;
pVal = NULL; //这是一个编程规范
课时16 动态内存分配-2
- 代码存放在代码区,数据则根据类型的不同存放在不同的区域中
- Bss段存放没有初始化或者初始化为0的全局变量。
- 大多数的操作系统在加载程序的时候会把bss全局变量清零,为了保证程序的可移植性,最好手工把变量初始化为0
- 例子bss.c
- $objdump -h | grep bss
- 在程序运行周期内,bss数据是一直都在的
- Data段存放初始化为非零的全局变量
- 参看例子 data.c
- $objdump -h data | grep data
- 仅仅是将初始值改为非0了,文件size就变大
- 同样,作为全局变量,在整个运行周期内,data数据一直存在
- 静态变量在第一次进入作用域时被初始化,以后不必再初始化
- 静态成员变量在类之间共享数据,也是放在全局/静态数据区中。并且只有一份拷贝
尽量不要定义全局变量
会导致代码的耦合度增加
较大的全局变量会增加程序文件体积
- rodata存放常量数据
- 常量不一定放在rodata中,有些立即数直接和指令编码在一起,放在text中。
- 字符串常量,编译器会去掉重复的字符串,保证只有一个副本。
- 常量是不能修改的
- 字符串会被编译器自动放到rodata中,加const关键字修饰的全局变量也放在rodata中
- char *p = "hello";p[1]='x';
- 栈中存储自动变量或者局部变量,以及传递的参数等
- 在一个函数内部定义了一个变量,或者向函数传递参数时,这些变量和参数存储在栈上,当变量退出这些变量的作用域时,这些栈上的存储单元会被自动释放
- 堆是用户程序控制的存储区,存储动态产生的数据
- 当用malloc/new来申请一块内存或者创建一个对象时,申请的内存在堆上分配,需要记录得到的地址,并且不需要的时候释放这些内存。
- 对象的生命周期是指对象从创建到被销毁的过程,创建对象时要占用一定的内存。因此整个程序占用的内存随着对象的创建和销毁动态的发送变化。
- 变量的作用域决定了对象的生命周期
- 全局对象在main之前被创建,main退出后被销毁
- 静态对象和全局对象类似,第一次进入作用域被创建,但是程序开始时,内存已经分配好。
- 作用域由{}定义,并不一定是函数
- 通过new创建对象,容易造成内存泄漏。通过new创建的对象一直存在,直到被delete销毁。
- 隐藏在中间的临时变量的创建和销毁,生命周期很短,容易造成问题。
拷贝构造函数,新建一个a的副本
A *foo(A a) {A *p = new A();return p;}
A *foo(const A& a) {A *p = new A();return p;}
下面的性能高,因为省去拷贝构造a的副本的过程
课时17 拷贝构造函数-1
- 拷贝构造函数(copy constructor)是一种特殊的构造函数,具有单个形参,此形参是对该类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。
- 当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式的调用拷贝构造函数。
- 如果一个类没有定义拷贝构造函数,编译器会默认提供拷贝构造函数。
定义
Student(const Student&); //拷贝构造函数
调用时机
Student s2 = ms; //用一个对象构造另外一个对象
fn(ms); //拷贝一个临时对象(作为函数参数)
课时18 拷贝构造函数-2
- 编译器提供的默认拷贝构造函数的行为
- 执行逐个成员初始化(memberwise initialize),将新对象初始化为原对象的副本
- “逐个成员”,指的是编译器将现有对象的每个非static成员,一次复制到正在创建的对象
- 为什么C++要定义拷贝构造函数
- s和t拥有同一资源,引起问题
- 浅拷贝
- 创建p2时,对象p1被复制给了p2,但资源并未复制,因此,p1和p2指向同一个资源
- 深拷贝
创建p2后,p1指针指向的资源也进行了复制,p2的指针指向了新创建的资源
- 何时需要定义拷贝构造函数
- 类数据成员有指针
- 类数据成员管理资源(如打开一个文件)
- 如果一个类需要析构函数来释放资源,则它也需要一个拷贝构造函数
- 如果想静止一个类的拷贝构造,需要将拷贝构造函数声明为private
课时19 const关键字
- 最常见的for循环代码
for(int index=0;index<512;index++)
{
}
- 代码存在的问题
- Magic Number,512代表什么含义
- 维护
- C语言
- BUFFER_LENGTH在预处理时被替换,编译器看不到
#define BUFFER_LENGTH 512
- C++提供了const限定符
const int BUFFER_SIZE=512;
- 指定一个不该被改动的对象
- const限定指针类型
- const出现在星号左边,表示被指物是常量
- const出现在星号右边,表示指针自身是常量
const int a = 1;
//a = 3; 错误
int b = 0;
b = 3;
const int *p = &a;
p = &b;
//*p = 1; 错误
int *const p2 = &b;
//p2 = &a; 错误
*p2 = 3;
- const数据成员必须使用成员初始化列表进行初始化
定义
class A{
public:
A();
~A(){}
private:
const int val;
};
错误初始化:
A::A()
{
val = 0;
}
正确初始化:
A::A()
:val(0)
{
}
- const成员函数
- 类接口清晰,确定哪些函数可以修改数据成员
class Data{
public:
Data:SIZE(0){val = 100}
int getValue() const {return val;} //表示不会修改val的值
private:
const int SIZE;
int val;
}
- 使用const提高函数的健壮性
- 以pass-by-reference-to-const替换pass-by-value
- 控制使用指针和引用传递的实参被意外修改
void foo(Person person){}
void bar(const Person &person){}
两个函数的语义都是一样的,表示不会对person进行修改,但是bar()函数效率更高,因为它少了一次拷贝构造和析构函数的过程。
课时20 友元函数与友元类
- 在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问。
- 友元(friend)机制允许一个类将对其非公有成员的访问权授予指定的函数或类。
- 友元的声明以关键字friend开始
- 只能出现在类定义的内部
- 可以出现在类中的任何地方,不是授予友元关系的那个类成员,所以不受其声明出现部分的访问控制影响
- 友元关系是授予的
- 为了让类B成为类A的友元,类A必须显式声明类B是他的友元
- 友元关系是不对称的
- 如果类A是类B的友元,类B是类C的友元,不能推出类B是类A的友元
- 友元会破坏封装
- 友元实例
class X{
public:
void initialize();
friend void g(X *,int);//Global friend
friend void Y::f(X *);//class member friend
friend class Z;//Entire class is a friend
friend void h();
private:
int i;
}
课时21 案例讲解-单例设计模式
- 案例介绍:保证应用程序中,一个类只有一个对象
- 案例设计:将构造函数设置为私有,在类的实现中确保生成对象的个数
把构造函数和析构函数声明为私有的,声明公有的getInstance获得实例。
//not thread safe,using pthread_mutex_lock/unlock
Signleton* Singleton::getInstance()
{
Singleton* ret = instance;
if(instance == NULL){
instance = new Singleton();
ret = instance;
}
return ret;
}
课时22 案例讲解-valgrind内存检测工具
案例介绍:使用工具检查程序中存在的内存问题
案例设计:使用valgrind完成相关功能
balgrind --tool=memcheck --show-reachable=yes --leck-check=yes 文件名
章节4 运算符重载
课时23 运算符重载
- 类是用户自定义的数据类型,使用运算符重载可以实现如下逻辑:
- 对象3=对象2+对象1,如描述复数的类,描述字符串的类
- 提高程序的可读性
- 重载赋值运算符
- 如果一个类没有提供赋值运算函数,则默认提供一个
- 如果一个类提供了拷贝构造函数,那么也要提供一个重载的赋值运算函数
- 重载赋值运算符
Person & Person::operate =(const Person &other) {
//检查自赋值
if(this == &other)
return *this;
//释放原有的内存资源
delete[] m_data;
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, ohter.m_data);
//返回本对象的引用
return *this;
}
- C++语言规定:
- 重载的运算符要保持原运算符的意义
- 只能对已有的运算符重载,不能增加新的运算符
- 重载的运算符不会改变原先的优先级和结合性
- 运算符重载的形式
- 成员函数
- 友元函数
- 运算符重载的限制
- C++规定,参数说明都是内部类型时,不能重载。例如不允许声明:
int* operator+(int, int*); //error
- 使用友元重载一元运算符
class RMB {
public:
RMB(unsigned int d, unsigned int c);
friend RMB operator+(RMB&, RMB&);
void display() {cout <<(yuan+jf/100.0)<<endl;}
private:
unsigned int yuan;
unsigned int jf;
}
- 代码实现
RMB operator+(RMB& s1, RMB& s2) {
unsigned int jf = s1.jf + s2.jf;
unsigned int yuan = s1.yuan + s2.yuan;
RMB result(yuan, jf);
return result;
}
课时24 成员函数重载运算符
- 运算符重载作为成员函数
- 作为成员的运算符比之作为非成员的运算符,在声明和定义时,形式上少一个参数。
RMB RMB::operator+(RMB& s)
{
unsigned int c=js+s.js;
unsigned int d=yuan+s.yuan;
return RMB(c,d);
}
- C++规定:=, (), []; ->这四种运算符必须为成员形式
课时25 自增运算符重载
- 前增量与后增量的区别
- 使用前增量时,对对象(操作数)进行增量修改,然后再返回该对象
- 使用后增量时,必须在增量之前返回原有的对象值
class Increase{
public:
Increase(int x):value(x){}
Increase & operator ++(); //前增量 返回的是引用
Increase operator ++(int); //后增量 返回的是对象 参数int只是为了区别前增量
private:
int value;
}
- ++ 的成员形式的重载
Increase & Increase::operator ++()
{
value++; //先增量
return *this; //再返回原对象
}
Increase Increase::operator ++(int)
{
Increase temp(*this); //通过拷贝构造函数保存原有对象值
value++; //原有对象增量修改
return temp; //返回原有对象值
}
前增量的效率比后增量的效率高,因为少了一个对象的构建
课时26 案例讲解-String类的运算符重载
- String类实现
- 案例介绍:String类是应用框架中不可或缺的类
- 案例设计:重载运算符实现字符串的操作
14行是拷贝构造函数
s1 += s3; // 调用的是22行
s3 += "!"; //调用的是25行
data ()是类的函数
章节5 继承
课时27 继承-1
- 生活中,继承的例子随处可见
动物
食草动物 食肉动物
- OOP三大特性
- 封装 encapsulation
- 继承 inheritance
- 多态 polymorphism
- 继承背后的思想就是基于已存在的类来构建新类
- 当从已存在类继承时,就重用了它的方法和成员,还可以添加新的方法和成员来定制新类以应对需求
- 约定:从其他类导出的类叫子类,被导出的类叫父类
- 继承在OO中不可或缺
- 类之间关系
- Is-a 继承体现
- Has-a 组合体现
- 继承的意义
- 代码重用
- 体现不同抽象层次
- 父子类关系
- 父类更抽象,更一般
- 子类更具体,更特殊
- 继承的特点
- 子类继承了父类的成员
- 具有层次结构
- 继承的优点
- 代码重用
- 父类字段和方法可用于子类
- 从抽象到具体形成类的继承体系
- 可轻松自定义子类
- C++中继承有三种方式
- 公有继承,私有继承,多重继承
class Teacher : public Person {
// 其余代码省略
}
Unified Modeling Language(UML)又称统一建模语言。
用UML来描述类之间的关系。
我们使用astah社区版免费软件画UML。
课时28 继承-2
代码示例
子类可以访问父类的公有和保护成员,但是不能访问父类的private成员
- 子类对象的内存布局
+-----------+ <--+
|Person部分 | |
+----------- + +---Teacher对象尺寸
|Teacher部分| |
+-----------+ <--+
Teacher内存布局
我们这里不深入, 深入C++对象模型 里有深入介绍
- 子类可以直接访问父类的保护数据成员
- 在构造一个子类时,完成其父类部分的构造由父类的构造函数完成
- 子类与父类的构造与析构的顺序
构造:父类-->子类
析构:子类-->父类
class Teacher: public Person
{
public:
Teacher(char* pName)
:Person(pName) //构造基类的部分
{
grade=0;
}
};
课时29 私有继承
- 私有继承的子类不能做基类能做的所有的事,因此私有继承的子类和父类之间不是“is-a”的关系。
#include <iostream>
class Animal {
public:
Animal(){}
void eat(){cout<<"eat\n";}
};
class Duck:private Animal{
public:
Duck(){}
void strechNeck(){cout<<"strech neck \n";}
}
- 私有继承的限制
class Cat:public Animal{
public:
Cat(){}
void Meaw(){cout<<"meaw\n"}
};
void func(const Animal& an){
an.eat();
}
void main(){
Cat kitty;
Duck duck;
func(kitty);
func(duck); //error
}
错误:‘Animal’是‘Duck’不可访问的基类
里式替换原则LSP:任何基类可以出现的地方,子类一定可以出现。
里式替换原则是开闭原则的补充。
开闭原则OCP:软件实体应该对扩展开放,对修改封闭。
- 不同继承方式的影响主要体现在:
- 派生类成员对基类成员的访问权限
- 通过派生类对象对基类成员的访问权限
- 三种继承方式
- 公有继承
- 私有继承
- 保护继承
- 公有继承(public)
- 基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
- 通过派生类的对象只能访问基类的public成员。
- 私有继承(private)
- 基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
- 通过派生类的对象不能直接访问基类中的任何成员。
- 保护继承(protected)
- 基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
- 通过派生类的对象不能直接访问基类中的任何成员
保护继承基本没有使用过。
- 继承访问控制表
这个做了解。
主要掌握公有继承是“is-a”的关系。私有继承不是“is-a ”的关系。
课时30 多重继承
- 可以为一个派生类指定多个基类,这样的继承机构被称为多重继承或多继承。
- 到目前为止,所讨论的类层次中,每个类只继承一个父类,在现实世界中事情通常是这样的。但是一些类却代表两个类的合成。
多继承是C++中有争议的一个点。
大家在使用的时候尽量避免多继承。
- 代码实现
#include<iostream>
class Bed {
//...
}
class Sofa {
//...
}
class SleeperSofa :public Bed, public Sofa {
SleeperSofa(){}
void FoldOut(){cout<<"Fold out the sofa.\n";}
}
- 当两个父类有同样的成员时会带来模糊性
void main()
{
SleeperSofa ss;
ss.SetWeight(20); //Bed的SetWeight还是SofaSetWeight?
}
- 这样导致了名称冲突(name collision),在编译时将予以拒绝
程序必须在重量前面说明基类:
void main()
{
SleeperSofa ss;
ss.sofa::SetWeight(20); //说明是沙发重量20斤
}
- 同一个沙发床有两个重量是不合理的
- SleepSofa对象内存布局
- 虚拟继承的内存布局
- 代码实现
class Furniture {
public:
Furniture(){}
void SetWeight(int i){weight = i;}
int GetWeight(){return weight;}
protected:
int weight;
};
class Bed :virtual public Furniture {
public:
Bed(){}
void Sleep(){cout<<"Sleeping...\n"}
};
虚拟继承也叫菱形继承
(为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。)
- 构造对象的规则需要扩展以控制多重继承。构造函数按下列顺序被调用:
- 任何虚拟基类的构造函数按照它们被继承的顺序构造
- 任何非虚拟基类的构造函数按照它们被继承的顺序构造
- 任何成员对象的构造函数按照它们声明的顺序调用
- 类自己的构造函数
课时31 案例讲解-继承与组合
继承与组合
- 案例介绍
- 编写模拟程序,模拟停车场,包括车,奔驰,变形金刚。
- 汽车有引擎,车轮等组成
- 案例设计
- 车,奔驰,变形金刚之间是继承关系
- 引擎,车轮与车之间是组合关系
优先使用组合
课时32 多态
- OOP三大特性
- 封装 encapsulate
- 基础 inheritance
- 多态 polymorphism
poly词根指多种形态
- 多态性指“多种行为”
- 同样的方法调用而执行不同操作、运行不同代码
- 多态通过分离做什么和怎么做,从另一个角度将接口和实现进行分离
- “封装”通过合并特征和行为来创建新的数据类型
- “实现隐藏”通过将细节“私有化”把接口和实现进行分离
- “多态”则消除了类型之间的耦合关系
- LSP(Liskov替换原则):子类型必须能够替换掉他们的基类型
指所有基类型出现的场景,子类型都可以使用。
- 多态的概念基于对象引用的动态绑定特性
- 多态实现过程:
- 子类重写父类的方法
- 代码中向父类型变量发出消息(静态绑定)
- 运行时,根据变量实际引用的对象类型决定调用哪个方法(动态绑定)
- 静态绑定在编译期进行
- 动态绑定在运行期进行
- 动态绑定是多态现象的根源
- 虚类与抽象类
class Animal{
virtual ~Animal();
virtual void makeSound();
}
C++支持虚函数,JAVA中的函数全部都是虚函数
(
通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
就是函数不是虚函数时父类引用调用子类对象的同名函数,调用的仍然是父类的函数。
父类函数声明为virtual,子类同名函数自动变成virtual。
)
虚函数在C++是通过虚函数表实现的。
- 纯虚函数与接口类
class Animal{
virtual ~Animal() = 0;
virtual void makeSound() = 0;
}
virtual void makeSound() const = 0;
后面加上等于0,表面该函数是纯虚函数,该类是抽象类,不能实例化。
类的所有函数都是纯虚函数,这样的类叫做接口类。
在C++中,接口类没有关键字支持。
- 接口类不能实例化,不能生成对象实例
课时33 实例-为多态基类声明虚析构函数
- 必须为多态基类声明virtual析构函数
- 为什么Animal类的析构函数必须是虚拟的?
析构函数可以是virtual,构造函数不能是virtual
如果通过基类的指针去执行析构函数,那么只会执行父类的析构函数,会造成问题。
- 针对接口编程,而不是针对实现编程
课时34 案例讲解-简单工厂
简单工厂
- 案例介绍
- 模拟终止园管理程序
- 种植园里种苹果,葡萄等水果
- 案例设计
- 使用简单工厂模式实现开闭原则
开闭原则:对于扩展是开放的,对于修改是关闭的
使耦合性降低
章节7 模板
课时35 类模板