CH15 面向对象程序设计
面向对象程序设计是基于三个基本概念的:数据抽象、继承和多态。
第7章介绍了数据抽象的知识,简单来说,C++通过定义自己的数据类型来实现数据抽象。
数据抽象是一种依赖于接口和实现分离的编程技术:类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。
封装是一项将低层次的元素组合起来形成新的、高层次实体的技术!函数和类都是封装的具体形式。其中类代表若干成员的聚集。大多数(设计良好的)类类型隐藏了实现该类型的成员。
一个简单的PersonInfo类,包含类成员、成员函数、构造函数
#include <string> using namespace std;
class PersonInfo {
public:
PersonInfo();
PersonInfo(string& name):strname(name),age(){}
string getName() { return strname; }
int getAge() { return age; } private:
string strname;
int age;
};
2.访问控制符
程序所有部分均可访问public成员
只有在类内部才能访问类的private成员,即类的private成员对外是不可见的。
3.成员函数可以被重载,上面的PersonInfo类就定义了两个版本的构造函数,被重载的成员函数之间的参数数量和类型不能完全相同,因为重载的成员函数被调用时会根据参数数量和类型进行匹配。重载构造函数的的原理和调用和普通成员函数的重载是一样的。
4.类的声明和定义
一个源文件中一个类只能定义一次。
可以只声明一个类,而先不定义它,一般,这种情况是有关联的类
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
StrBlob();
StrBlob(initializer_list<string>il){}
//其它成员函数
StrBlobPtr begin();
StrBlobPtr end();
private:
//私有成员
};
但是只声明没有定义的类,不能使用,即:不能定义该类的对象,因为生命之后,定义之前,StrBlobPtr是不完全的类,并不知道这个类有哪些成员。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。,如上面的begin()和end()函数。
5.类对象
定义一个类后,就可以定义该类的对象,可以认为对象是类的实体,类通过对象实现一系列操作。定义对象时,会为其分配内存空间,定义类型时并没有进行存储分配。
//两种定义对象的方式都可以,一般是第一种
PersonInfo a;
class PersonInfo b;
6.this指针
成员函数有一个隐形形参,指向该类对象的一个隐形形参,即this指针,this的形参是由编译器定义的,与成员的当前对象绑定在一起
class Screen {
public:
typedef string::size_type pos;//此处类为自己定义了一个局部类型,使用typedef为string::size_tyoe 定义了个别名
//为类做了一个更好的抽象,
Screen() = default;
Screen(pos ht, pos wd,char c):height(ht),width(wd),contents(ht*wd,c){}//define constructor and initizalize
char get()const { return contents[cursor]; }
inline char get(pos ht, pos wd)const;//declare operator member function
Screen &move(pos r, pos c);//Note:此处返回的是引用,该引用指向执行操作的那个对象
//add some function to set the char in the position pointed by the cursor
//和move()一样,set成员的返回值是调用set对象的引用,返回引用的函数是左值的
Screen &set(char);
Screen &set(pos, pos, char);
private:
pos cursor = ;
pos height = , width = ;
string contents;
}; //define move()
//勒种一些规模较小是函数适合被声明为内联函数,类内部的默认自动是inlien 的。
inline Screen &Screen::move(pos r,pos c)
{//函数返回调用自己的对象,使用this 来访问该对象
pos row = r*width;//cacculate the position of the row
cursor = row + c;//move the cursou to the pointed column in the row
return *this;//return the object } char Screen::get(pos r, pos c)const
{
pos row = r*width;
return contents[row + c];
}
inline Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;//返回的是this指向的对象,将this对象作为左值返回
}
//重载的set()函数
inline Screen& Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch;
return *this;
}
int main()
{ Screen myScreen; myScreen.move(, ).set('*');//这句相当于下面两句,
myScreen.move(, );
myScreen.set('*');//,
system("pause");
return ;
}
Note: set()成员不能定义为const 的,因为set()返回的是调用set()对象的引用,返回的是左值,而左值必须是可以修改值的,不能是const 的
下面我们定义一个函数从const 成员返回*this
在普通的非const成员函数中,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
不能从const成员函数返回指向类对象的普通引用。const成 员函数只能返回*this作为一个 const引用。
定义一个display 成员打印出Screen的内容,只是显示Screen的内容不需要改变,因此将const定义为一个const成员,此时this 是一个指向const的指针,*this 就是const对象 ,所以display的返回类型应该是const Screen&,但是,如 如果令display返回一个const引用,则我们不能把display嵌入到一组动作的序列中
此时若用display返回的对象调用set()成员,就会发生错误,,上面说了set成员为什么不能设为常量、
所以下面调用是错误的
myScreen.dipaly(cout).set('#');
Note:一个const成员函数如果以引用方式返回*this,那么它的返回类型将是常量引用。所以定义一个非const的display成员
此处注意,定义了一个小的私有成员,do_play(),看似没简化操作,实则,这样的小程序段是非常有用的,关于这样公共代码使用私有功能函数的建议,课本P248有详细说明
class Screen {
public:
//其它以定义的public成员
const Screen &dipaly(ostream& os)const { do_display(os); return *this; }
Screen& dispaly(ostream&os) { do_display(os); return *this; }
private:
//其它以定义的prvate成员
void do_display (ostream &os)const { os << contents; }
};
7.类作用域
名字查找与类的作用域
类的定义分两步
1.编译成员的声明
2.直到类全部可见后才编译函数体
编译器处理完类中的全部声明后才会处理成员函数的定义
OOP概述
1.OOP核心思想是数据抽象,继承和动态绑定,使用数据抽象可以将类的接口与实现分离,使用继承可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,以统一方式使用它们的对象
2.定义基类和派生类
其中Quote类是基类,Bulk_quote类是派生类。派生类的构造函数必须重新写过,不能继承。(因为毕竟两个类的类名都不一样,不可能构造函数继承)只继承其他的成员函数和成员变量。
派生类可以覆盖基类的虚函数,但是也可以选择不覆盖(即直接使用父类的函数版本)
每个类控制它自己的成员的初始化过程,派生类初始化成员时,对于从父类继承来的成员必须使用父类的构造函数进行初始化,对于自己新增加的成员,调用自己的构造函数初始化
Note:基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
#include <string>
//#include <cstddef>
#include <iostream>
using namespace std;
//基类
class Quote {
public:
//将默认构造函数声明为default,则要求编译器为嗯合成默认构造函数
Quote() = default;
//重载constructor
Quote(const string &book,double sales_price):bookNo(book),price(sales_price){}
string isbn()const { return bookNo; }
//返回某种书的销售总额
//派生类会根据不同情形重写该函数,所以定义为虚函数
virtual double net_price(size_t n)const { return n*price; }
virtual ~Quote() = default;
private:
string bookNo;
protected:
double price = 0.0;
};
//派生类,从基类Quote那里继承了isbn()和bookNo和price,重新定义了net_price()函数
//新增加了min_qty 和discount 成员
class Bulk_quote:public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const string&, double, size_t, double);
double net_price(size_t)const override;
private:
size_t min_qty = ;//可以使用指定折扣的最低数量
double discount = 0.0;//折扣
};
Bulk_quote::Bulk_quote(const string& book, double p, size_t qty,double disc):
Quote(book,p),min_qty(qty),discount(disc)
{
//注意初始化列表中对成员bookNo 和price的初始化,不是像之前那样bookNo(book),price(p)
//进行初始化,而是调用基类的构造函数进行初始化
}
double Bulk_quote::net_price(size_t cnt)const
{
if (cnt >= min_qty)
return cnt*price*( - discount);
else
return cnt*price;
}
//dynamic binding
double print_total(ostream &os, const Quote &item, size_t n)
{
//根据传入的item形参的对象类型调用net_price()
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() <<'\t'<< "#sold: " << n << " total due: "
<< ret << endl;
return ret;
}
int main()
{
Quote item("978-7-121-31092-8", );//调用基类构造函数
Bulk_quote bulk("978-7-121-31092-8", , , 0.2);//call the constructor of sub class
//bookA.net_price(20);
//Quote item;//基类对象
//Bulk_quote bulk;//派生类对象 Quote *p =&item;
p->net_price();//dynamic binding
// Quote base;
// Bulk_quote subcls;
print_total(cout, item, );
print_total(cout, bulk, );
system("pause");
return ;
}
继承与静态成员
如果基类有一个静态成员,那么基类和所有派生类(包括派生类的派生类)都共同拥有这仅有的一个静态成员。并且,对该成员的访问控制与非静态成员的访问控制方式一样,即,如果是private的,则派生类无权访问,,如果是可访问的,则既可以通过基类使用它,也可通过派生类使用它。
class Base {
public:
static void statmem();//static member
};
class Derived :public Base {
void f(const Derived&);
}; void Derived::f(const Derived& derived_obj)
{
Base::statmem();//correct:difine in Base
Derived::statmem();//correct: Derived derived it from Base
derived_obj.statmem();//correct:visit it throgh the object of Derived
statmem();//correct: visit it through object of pointed by this
}
类型转换与继承
可以将基类的指针或引用绑定到派生类对象上(上面Quote例子中的63,64行):当使用基类引用(或指针)时,实际上我们并不清楚该引用(指针)所绑定的真实类型,该对象可能是基类的对象,也可能是派生类对象。
静态类型在编译时总是已知的,而动态类型的对象直到运行时才可知。
3.虚函数
我们必须为每一个虚函数都提供定义,不管它是否被用到了,因为编译器无法确定到底会使用哪个虚函数。
OOP的核心思想就是多态(polymorphism):具有继承关系的多个类型称为多态类型。
派生类中的虚函数,当在派生类中覆盖了基类的某个虚函数,可以使用virtual关键字指明,即使不这么做,C++也是默认virtual的。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配,当类的虚函数返回类型是类本身的指针或引用时例外。
只有虚函数才能被覆盖
基类的虚函数默认实参最好与派生类的虚函数默认实参一致。
因为如果通过基类的引用或指针调用函数,则将使用基类的默认实参版本,但是该函数的实现是动态绑定的,即可能是基类的函数也可能是派生类的函数。
struct B {
virtual void f1(int)const;
virtual void f2();
void f3();
};
struct D :B {
void f1(int)const;//correct:f1matched with f1 in class B
void f2(int)override;//incorrect:theris no f2(int) in class B
void f3()override;//incorrect:f3 is not a virtual function
void f4()override;//incorrect:there is no function named f4 in class B
};
4.抽象基类
纯虚函数:纯虚函数不需要定义,在声明时写上=0即可,智能出现在虚函数声明处的语句。
含有(或未经覆盖而直接继承)纯虚函数的类为抽象类,抽象基类负责定义借口,而后续其它类可以覆盖接口。Note:不能创建抽象基类的对象
如将之前的net_price()定义为虚函数
class Dis_quote :public Quote{
public:
Dis_quote() = default;
Dis_quote(const string& book, double p, size_t qty, double disc) :
Quote(book, p), quanty(qty), discount(disc)
{ } double net_price(size_t)const = ;//pure virtual
protected:
size_t quanty = ;
double discount = 0.0;
};
Disc_quote discounted;//incorrect :can not define the object for abstract base class
Disc_quote 的派生类必须定义自己的net_price(),否则仍是抽象基类
现在可以重新定义Bulk_quote,让它继承Disc_quote而不是直接继承Quote
class Bulk_quote :public Disc_quote {
Bulk_quote() = default;
Bulk_quote(const string& book,double p,size_t qty,double disc):
Disc_quote(book,p,qty,disc){}
//overeide net_price() in class base
double net_price(size_t)const override;
};
访问控制与继承
对于访问控制,记住基类的私有成员,不管派生类的继承方式是什么,都是不可见的。派生类可以改变基类中的成员的访问权限,也只限于可访问的成员。
6. 继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类中的作用域内,如果一个名字在派生类的作用域内是无法解析的,则编译器将继续在外层的基类作用域寻找该名字,并且是在编译时进行名字查找;当派生类
的成员名字与基类成员名字冲时,此时定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)的名字
#include <iostream> struct Base {
Base():mem(){}
protected:
int mem; };
struct Derived :Base {
Derived(int i) :mem(i) {}//用i初始化mem,
//Base::mem 进行默认初始化
int get_mem() { return mem;}//返回Derived::mem
protected:
int mem;//与基类成员名形同,会隐藏基类中的mem
};
int main()
{
Derived d();
std::cout << d.get_mem() << std::endl;//打印结果42,说明调用get_mem()
//对mem的解析结果是定义在Derived中的
system("pause");
return ; }
CH15 面向对象程序设计的更多相关文章
- [.net 面向对象程序设计深入](0) 开篇
[.net 面向对象程序设计深入](0)开篇 [.net 面向对象编程基础]和 [.net 面向对象程序设计进阶]在15年底写完了,群里也加进来不少热爱学习的小伙伴.让我深切感受到在这个 ...
- [.net 面向对象程序设计进阶] (1) 开篇
[.net 面向对象程序设计进阶] (1) 开篇 上一系列文章<.net 面向对象编程基础>写完后,很多小伙伴们希望我有时间再写一点进阶的文章,于是有了这个系列文章.这一系列的文章中, 对 ...
- [.net 面向对象程序设计深入](6).NET MVC 6 —— 模型、视图、控制器、路由等的基本操作
[.net 面向对象程序设计深入](6).NET MVC 6 —— 模型.视图.控制器.路由等的基本操作 1. 使用Visual Studio 2015创建Web App (1)文件>新建> ...
- [.net 面向对象程序设计深入](5)MVC 6 —— 构建跨平台.NET开发环境(Windows/Mac OS X/Linux)
[.net 面向对象程序设计深入](5)MVC 6 —— 构建跨平台.NET开发环境(Windows/Mac OS X/Linux) 1.关于跨平台 上篇中介绍了MVC的发展历程,说到ASP.NET ...
- [.net 面向对象程序设计深入](4)MVC 6 —— 谈谈MVC的版本变迁及新版本6.0发展方向
[.net 面向对象程序设计深入](4)MVC 6 ——谈谈MVC的版本变迁及新版本6.0发展方向 1.关于MVC 在本篇中不再详细介绍MVC的基础概念,这些东西百度要比我写的全面多了,MVC从1.0 ...
- [.net 面向对象程序设计深入](3)UML——在Visual Studio 2013/2015中设计UML活动图
[.net 面向对象程序设计深入](3)UML——在Visual Studio 2013/2015中设计UML活动图 1.活动图简介 定义:是阐明了业务用例实现的工作流程. 业务工作流程说明了业务为向 ...
- [.net 面向对象程序设计深入](2)UML——在Visual Studio 2013/2015中设计UML用例图
[.net 面向对象程序设计深入](2)UML——在Visual Studio 2013/2015中设计UML用例图 1.用例图简介 定义:用例图主要用来描述“用户.需求.系统功能单元”之间的关系. ...
- [.net 面向对象程序设计深入](1)UML——在Visual Studio 2013/2015中设计UML类图
[.net 面向对象程序设计深入](1)UML——在Visual Studio 2013/2015中设计UML类图 1.UML简介 Unified Modeling Language (UML)又称统 ...
- [.net 面向对象程序设计进阶] (28) 结束语——告别2015
[.net 面向对象程序设计进阶] (28) 结束语——告别2015 <.net面向对象程序设计进阶>这一系列文章写了太长的时间了,大概有半年没写,在年底又一口气写了好几篇.在整个过程中目 ...
随机推荐
- 「题解」Just A String
目录 题目 原题目 简易题意 思路及分析 代码 题目 原题目 点这里 简易题意 现定义一个合法的字符串满足将其打散并任意组合之后能够形成回文串. 给你 \(m\) 种字母,问随机构成长度为 \(n\) ...
- 【渗透测试】NSA Windows 0day漏洞+修复方案
这个漏洞是前段时间爆出来的,几乎影响了全球70%的电脑,不少高校.政府和企业都还在用Windows服务器,这次时间的影响力堪称网络大地震. ------------------------------ ...
- Spring Boot 2.x 入门前的准备-IntelliJ IDEA 开发工具的安装与使用
常用的用于开发 spring boot 项目的开发工具有 eclipse 和 IntelliJ IDEA 两种,最近有声音提出 visual code 也开始流行开发 java,而且确实如此, vs ...
- zk的单机部署,与客户端的使用
下载zk wget https://archive.apache.org/dist/zookeeper/stable/apache-zookeeper-3.5.5-bin.tar.gz 安装jdk t ...
- dp求解各种子串子序列
目录 概念 最长上升子序列 最长连续子串 最长公共子序列 最长公共上升子序列 注:dp可能并不是求解该这些问题的最优算法,这里只是做一个dp 算法的简介. 概念 定义:假设现有一个 string = ...
- CAN数据格式-BLF
欢迎关注<汽车软件技术>公众号,回复关键字获取资料. Vector工具录制的数据,一般有ASC和BLF两种格式,本文介绍ASC. 1.BLF定义 BLF(binary logging fo ...
- preg_replace相关问题
preg_replace preg_replace 函数执行一个正则表达式的搜索和替换. 语法: preg_replace ( mixed $pattern , mixed $replacement ...
- python 连接oracle基础环境配置方法
配置基础: 1.python3.7 2.oracle server 11g 64位 3.PLSQL 64位 4.instantclient-basic-windows.x64-11.2.0.4.0这个 ...
- 关于程序状态字寄存器PSW(Program Status Word)与多核多线程
内核态(Kernel Mode)与用户态(User Mode) CPU通常有两种工作模式即:内核态和用户态,而在PSW中有一个二进制位控制这两种模式. 内核态:当CPU运行在内核态时,程序可以访问所有 ...
- Linux命令:grep命令 | egrep命令
grep:文本搜素工具,根据用户指定的文本模式对目标文件进行逐行搜索,显示能被模式所匹配到的行 包含三个命令:grep.egrep(相当于grep -E 扩展的正则表达式)和fgrep(相当于grep ...