txt文档转存

第十章 对象和类

构造函数,默认构造函数(没参数),编译器生成的构造函数,构造函数初始化器,复制构造函数,构造函数初始化列表,析构函数,赋值运算符
OOP特性:抽象,封装和数据隐藏,多态(重载),继承,代码的可重用性

OOP 考虑数据的表示和使用。数据类型+操作,定义+实现接口 和 数据存储

使用对象只是为了帮助管理代码,而不是为了使用而使用–避免过度使用,连一个uml都很简陋的那种
指望一个类吃遍天下也不可行–避免过度通用,你又不是在写STL

类-组件(更小,更具体)-属性-行为

2.抽象和类
指定 基本类型 要完成的三项工作:
决定数据对象需要的内存数量
决定如何结识内存中的位
决定针对对象执行的操作和方法
C++中的类
类规范:
类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口
类方法定义:描述如何实现成员函数

类定义–接口放在头文件中; 实现–类方法的代码。放在源代码文件中
类–对象/实例

  1. 访问控制:private-该类可访问,派生类不行/public/protected-派生类也行
    使用类对象可以直接访问公有部分public
    通过公有成员函数或友元函数访问私有成员private
    公有成员函数是程序和对象的私有成员之间的桥梁

    数据部分通常放在私有部分,接口的成员函数放在公有部分

    类对象默认private,强调的可以标出来
    结构的默认访问类型为public
    //first step
    class className
    {
    private:
    data member declarations
    public:
    member function prototypes
    };
    //second step 实现函数,可以在类声明中就给出定义而不是声明原型,但通常还是单独定义,除非
    函数太小
    2.成员函数的实现
    不改变对象的成员函数声明为const 如 double getValue() const;
    使用作用域解析运算符 :: 来标识函数所属的类
    类方法可以访问类的private组件–接口

定义位于声明中的函数 都将自动成为内联函数。类声明通常将短小的成员函数作为内联函数。
也可以在类声明之外定义内联函数----使用inline限定符    在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符
同类的不同对象,数据占据不通过的内存块,但是调用的方方都在同一个代码块
调用成员函数--> 发送消息
将同样的消息发送给不同的对象将调用同一个方法,应用于不同的对象

10.3 类构造函数和析构函数

可以无参数,也可以所有参数都设置默认值。必须提供构造函数的具体实现。(因此构造函数参数不能和类变量同名,可以在数据成员名前加前缀m_, 或者加后缀 _ )
构造函数没有返回类型

显式地调用构造函数:className object = func();–>定义了一个对象,并用构造函数func()对成员变量初始化
隐式地调用构造函数:className object (…)–>与上面那个等价
与new一起用:className *p_object = new func(…);–>创建一个对象,并初始化赋值,并将对象地址赋给p_object
这样,对象是没有名称的,但是可以使用指针来管理该对象

无法使用对象调用构造函数

默认构造函数 未提供显式初始化值,用它来创建对象的构造函数。
如果没有提供任何构造函数,C++自动提供默认构造函数,它是默认构造函数的隐式版本,不做任何工作。
例如,默认构造函数长这样 object::func() {}. 所以className object_1 创建新的对象,但不初始化成员 ,和int x, 不给值一样

当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数
如果为类定义了其他构造函数,则需要额外提供默认构造函数, 不然类似 int x 这样的语句就会报错。这样是为了禁止 创建未初始化的对象。但是你非得不初始化,则不接受任何参数的默认构造函数是必须的。

创建默认的构造函数两种方法
1.给已有的构造函数提供默认值。。。啊这,
2.通过函数重载来定义另外一个函数–没有参数的构造函数,与构造函数同名就行
两种方法选一个
e.g.
通常需要初始化一个已知的合理值,因此用户定义的默认构造函数通常给所有成员提供隐式初始值
className::func()
{
给成员赋值
}
当然,你不准备提供初始值,则可以这样 className() = default; 这样你就不用自己实现,而是交由编译器实现构造函数了

设计类时,应提供对所有成员变量做隐式初始化的默认构造函数,声明对象变量时,就不用显式初始化他们

Stock first; //隐式调用默认构造函数
Stock first = Stock(); //显式调用
Stock *p = new Stock;// 隐式调用

析构函数

对象过期,程序自动调用一个特殊的成员函数--析构函数
(什么都不做的析构函数,编译器可以自己生成)

析构函数在类名前加上 ~
//原型
~className();
//定义
object::~objct()
{
//可以加点东西,显示
}

编译器决定什么时候调用析构函数,不应该显式调用析构函数
静态存储类:程序结束的时候自动调用
自动春畜类:程序执行完代码块时调用–该对象是在代码块内定义的
new创建的:它将驻留在栈内存 或者自由存储区,当使用delete来释放内存时候,析构函数自动调用
临时对象–程序创建的:结束对该对象的使用时候,自动调用其析构函数

类对象过期调用析构函数,必须要有。如果程序员没提供,编译器自己声明一个默认的,并在发现删除对象的代码后,提供默认析构函数的定义—真是狗,不到最后不给定义占空间
e.g.
//头文件
#ifndef OBJECT_H_
#define OBJECT_H_
#include
//class
class object
{
private:
std::string str;
int a;
void func(…);
public:
//两个构造函数
object(); //默认构造函数
object(…);//接收参数的构造函数
~object(); //析构函数
void func_1(const std::string & b);
void func_2();
}

//实现
#inlcude < iostream>
#include “object.h”
object::object()
{
//变量设置为默认值
}
obiect::object(const std::string & co, long n, …) //接收参数
{
//接收的值赋给成员变量
}
object::~object()
{
//啥都不用写
}
void object::func_2(paramete)
{
using std::cout;
//body
}

//客户文件–测试用
#include < iostream>
#include “object,h”
int main()
{
//跳出这个代码块后,调用析构函数,不然只能等main结束后调用析构函数
//不加大括号,意味着黑框框关闭前,不会调用析构函数,导致无法看到析构函数的消息,如果有的话
{
using std::cout;
object object_1 (argument); //隐式调用构造函数
object.show();
object object_2 = object (argument); //显式调用构造函数
object_1 = object(argument); //临时对象
}
return0;
}
显式调用构造函数 一种是直接创建对象,并初始化,另一种是先创建一个临时变量,之后将内容复制到要创建的类
,立即或者过一会儿调用析构函数
object object_1 = object(argument)—初始化,可能会创建临时对象—效率高高哒
object_1 = object(argument)—赋值,总是会赋值前创建一个临时对象

####C++11列表初始化
只要提供和某个构造函数参数列表匹配的内容,用大括号括起来 就可以
object object_1 = {argument}//显示调用构造函数 用的是括号
//在这里,等于号可以省略
object object{};//与默认构造函数匹配

还有std::initializer_list(初始化列表)的类 可当做函数参数或方法参数的类型,这个类可以表示任意长度的列表。只需要所有列表项的类型都相同或者 可转换为相同的类型
#include < initializer_list>
object::object(initializer_list< double> args>)
{
/***/
}

const 成员函数
将声明变为:
void func() const;
实体为:
void object::func() const
通过以上来保证不会修改const 类,定义的函数被称为const成员函数
就像尽可能将const 引用和指针作为函数形参一样, 只要类方法不修改调用对象,就应该将其声明为const

10.4 this指针–涉及到多个对象

const object & func(const object & s) const;
参数-**显式访问另外一个对象**,**隐式访问改方法所在**的对象
参数const,不修改显式访问的对象
括号后const,函数不修改隐式访问的对象
该函数返回了两个const之一的引用,返回类型也就是const
const object & object::func(const object & s) const
{...return s; //参数对象 ...return *this; //隐式调用的函数所在对象
}
不用this指针,隐式调用的对象没有别名怎么返回。
**this指针指向用来调用成员函数的对象**(this被作为隐藏参数传递给方法)
每一个成员函数,包括构造函数和析构函数,都有一个this指针,指向调用对象的地址。如果方法需要引用整个调用对象可以用 ***this--作为调用对象的别名**  (this本身是地址)

10.5 对像数组
同一个类的多个对象,就成了数组嘛 和基本数据结构一样
object object_1[N];//调用默认构造函数
这个类要么没有显式定义任何构造函数(此时,使用隐式构造函数),要么定义了显式默认构造函数。
调用方法 object_1[num].func();
const object *p = object_1[num].func[object_1[num+1])
可以用构造函数初始化对象数组元素,但是每个元素都得调用一遍构造函数
列表初始化这个时候就是个好东西了
const int n = 4;
object object_1[n] = {
object(argument),

object(), //当类存在多个构造函数的时候,可以对不同元素使用不同的构造函数
}
初始化对象数组方案:先用默认构造函数创建数组元素,然后花括号中的构造函数们创建临时对象,接着,将临时对象复制到相应元素中。因此,要创建类对象数组,类必须有默认构造函数。

10.6 类作用域
类中名称,作用域是整个类,类外不可知。所以,可以在不同类使用同名
声明类 相当于声明了一个新的数据结构,没有实体。
要想常量伴随类,使得所有对象都可使用
1.在类中声明一个枚举,–不会创建类数据成员,所以,所有对象中都不包含枚举。类似define
class aa
{
private:
enum {Months = 12}; //只当做符号常量,因此不用提供枚举名
}
2.
calass aa
{
private:
static const int Months = 12; //静态变量,一开始就有,const,不可改变
}

要想类中枚举
enum class name{…}; //struct此时可以代替class
name choice = name::no1; name枚举中的第一个

    C++11 有些情况下,常规枚举类型自动转换为整型,如赋给int变量 或者比较表达式但作用域内枚举不能隐式地转换为整型,显式可以:int a = int(name::no1);默认情况下,类作用域内枚举的底层为int,但是,可以选择其他enum class : short name{...}; // short替代int成为底层类型。但是依然必须是整型,不能是double等

第十一章 使用类

11.1运算符重载–实现多种功能

重载运算符之前,需使用运算符函数:operatorop(argument-list)
e.g.
operator+(),重载 + 运算符
op必须是C++的运算符
operator 将重载[]运算符,[]是数组索引运算符
object_1 = object_2 + object_3;
编译器改为:
object_1 = object_2.operator+(object_3); //显式调用object_3(参数),隐式调用object_2

Time Time::sum(const Time & t) const
{
Time sum;
return sum;
}
total = coding.sum(fixing)-----1

参数引用,提高效率;返回类型不是引用,,函数创建了一个新的对象,返回它,函数将创建该对象的副本,调用函数可以使用它。而如果也返回引用,新对象的作用域局限于该函数,函数结束,对象销毁。
重载运算符的函数头
Time Time::operator+(const Time & t) const
{
同上
}
total = coding.operator+(fixing) //函数表示法----2
total = coding + fixing //运算符表示法,+ 左侧是调用对象,右侧是传递的参数-----3
以上三种效果一样

**operator+()**函数的名称使得可以使用函数表示法或运算符表示法来调用它,编译器根据操作数的类型来决定如何做 因此 调用该方法时 t1 = t2 +t3 +t4 合法

11.2.2重载限制

1.重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。所以 - 号不能重载为两个基本数据类型的加和生生的指 “-”为“+”
2.运算重载符不可以改变语法结构(求模%不可重载成其他用途)。不可以改变操作数,不可以改变优先级,不可以改变结合性,不可以创造新的预算符, 相当于现有的运算符运算的数据类型是用户定义的外,其余不变
3. = 赋值运算符 ()函数调用运算符 []下标运算符 ->通过指针访问类成员的运算符 只能通过成员函数重载 其余都可以通过成员函数或者非成员函数重载
4.最好保留运算符原有的基本功能

11.3友元-Friend

C++控制对类对象私有部分的访问,通常,公有方法提供唯一的访问路径
但是友元可以提供与该类相同的访问权限:友元函数,友元类,友元成员函数

为类重载二元运算符,常需要友元
A = B.operator*(2.75) 对应于 A = B * 2.75, 左侧操作数应是调用对象
但反过来A = 2.75 * B 但是2.75 不是对象,不能调用成员函数来替换该式子
解决法一,只能按照B2.75写
法二,非成员函数–它不是由对象调用。使用的所有值,包括对象都是显式参数
A = 2.75 * B----->>>>> A = operator
(2.75,B)
对应原型 Time operatpr*(double m, const Time & t)
但是因为不能直接访问类的私有数据,因此需要友元函数

11.3.1 创建友元函数—friend
e.g.
friend Time operator*(double m, const Time & t);
1.虽然是在类中声明,但他不是类成员函数,不能用成员运算符调用
2.它与成员函数有着相同的访问权限
Time operator*(double m, const Time & t) // friend 不需要出现在定义中,也不实用::限定符
{
和之前一样
}
这样 2.75 * B 和B * 2.75 都找到各自的成员数

Time operator*(double m, const Time & t)
{
return t * m; //使用t.operator(m)
}
这样可以交换乘法操作数的顺序,将友元函数编写为非友元函数
该函数作为友元,可以当作类的正式接口之一,如果需要直接访问私有数据,只需要修改函数定义
tip,如果需要为类重载运算符,并将非类的项作为第一个操作数,可以使用友元函数来反转操作数顺序

11.3.2 常用的友元:重载 <<运算符

重载 << 使之能与cout一起来显示对象的内容
版本1:
使用友元函数,
谁重载,谁是第一个操作数 ,如Time类trip对象,输出trip对象需要重载cout
这就成了trip << cout
但是 可以通过友元函数重载
void operator<<(ostream & os, const Time & t)
{
os << t.hours ;
}
相当于借着友元函数不需要对象调用,想办法实现正常的输出功能

Time类声明operator<<()函数是它的一个友元函数,但不是ostream类的友元
operator()<<使用了Time类的数据,所以,必须是Time类的友元 同时ostream对象作为一个整体在使用,所以不必是友元

另外,ostream 对象 cerr将输出发送到标准输出流——默认显示器,也可以将标准错误流重定向到文件
ofstream对象可将输出写入文件,通过继承,ofstream可以使用ostream方法,这样operator<<()定义类数据写到文件和屏幕上,因此只需要传递经过适当初始化的ofstream对象
调用cout<<trip使用cout对象本身,而不是拷贝,因此需要按引用来传递对象。这样os成为cout的一个别名

版本2:
因为即使重载后cout<<trip 可行
但是cout <<“haha” <<trip 仍然不行
这就需要就每次cout 都返回一个指向ostream对象的引用–返回一个指向调用对象的引用
ostream & operator<<(ostream & os, const Time & t)
{
os << t,hours <<" haha" <<t.minutes;
return os; // 返回指向调用对象的引用
}
此时cout << trip 相当于operator<<(cout, trip)—返回一个operator &

11.4 重载运算符:作为成员函数还是非成员函数
非成员函数就应该是友元函数
Time operator+(const TIme & t) const;//成员函数 ,一个操作数通过this指针隐式传递,另一个操作数作为参数显示传递
friend Time operator+(const TIme & t1, const Time t2);//友元函数,二元运算符

以上两种只能选其一
11.5 矢量类 vector
随机数:
rand()返回0到某个数之间随机整数,使用一个初始种子通过算法生成随机数,该随机数当做下一次生成的种子。但是,实际上这是伪随机数,通常生成的可能都是一样的值
srand()允许覆盖默认的种子,重新启动另外一个随机数序列。使用time(0)的返回值来设置种子, time(0)返回当前日期从0点开始的秒数(扩大解释:time()接收time_t变量的地址,将时间放到该变量中,并返回他,将0用作地址参数,可以省略time_t变量声明)因此srand(time(0))每次运行程序都将设置不同的种子,使得随机数看上去更为随机 rand 和srand共用了种子?
rand 有返回值, srand()没有返回值 (cstdlib ctime)

结果输出:
包含<fstream>,并声明一个ofstream对象,并与一个文件关联
#include <fstream>
ofstream fout;
fout.open("file.txt")
fout << result << endl;//调用友元函数operator<<(fout,result),导致引用参数os指向fout,从而输出文件

11.6 类的自动转换和强制类型转换

互相兼容的数据类型,C++将自动转换,保留或降低精度

基本数据类型-转换为类对象 --类函数
classname object;
object = 15.6; //使用了定义的object(double)来初始化
程序使用定义好的构造函数创建临时的无名classname类对象,将15.6作为初始值, 随后采用逐成员赋值的方式将临时对象的内容复制到object中–隐式转换,自动进行
只有接收一个参数的构造函数才能作为转换函数

强制转换int(2.5)和(int)2.5 都可以。但是如果第二个参数提供默认值,如
object(int stn,double lbs = 0) 则可以,实际上还是只能输入一个参数值

当然,当你需要关闭自动转换,这样做
explicit object(double lbs);//关闭隐式转换,但显式强制转换仍然ok
object = classNmae(19.6)//ok
object = (classNmae)19.6 //ok
object = 19.6 //ng

转换函数作用时机:
用了explicit 只能显式强制类型转换,否则以下可以隐式转换:
对象初始化为double时;
double值传递给对象时;
double值传递给接受对象作为参数的函数时;
返回被声明为对象的函数试图返回double时;
上述情况,使用了可以转换为double,且为对该类型定义新函数时,如没有针对int的函数,则int转double后转对象

11.6.1 转换函数

–运算符函数–operator-类转换到基本数据类型
int可以转化为对象,反之也可以。构造函数只用于从某种类型转换到类类型,相反的操作需要转换函数–用户定义的强制类型转换没定义该函数就报错
double lbs = double(object);//手动强制转换
double lbs = (double)object //以上两个显示
double lbs = object;//让编译器决定,隐式

转换函数的定义:
operator typeName();
1.转换函数必须是类方法–需要通过类对象来调用,从而告知函数要转换的 值
2.不能指定返回类型
3.不能有参数;
e.g.
operator double(); 然后将对象中数据返回……

在没有提供显示强制类型转换的情况下,程序不会自动转换(二义性报警,因 为有多种可能),除非定义了唯一可行的转换函数 则自动隐式的来转换

当你不希望转换函数自动运行时,这么做
explicit operator typeName();//只能强制转换
或者
int object::obj_to_int(){return int(pounds + 0.5);}替换掉
object::operator int(){return int(pounds + 0.5);}
这样int plb = object非法
int plb = object.obj_to_int()就行
P419
11.6.2 转换函数与友元函数
成员函数重载加法:P420
Stonewt Stonewt::operator+(const Stonewt &st) const
{
double pds = pounds + st.pounds;
Stonewt = sum(pds);
return sum;
}
友元函数实现
Stonewt operator+(const Stonewt & st1, const Stonewt & st2) const
{
double pds = st1.pounds + st2.pounds;
Stonewt sum(pds);
return sum;
}
二选一,均可以实现以下
Stonewt jest(9,12);
Stonewt best(12,8);
Stonewt total;
total = jest + best;
如果有Stonewt(double)构造函数,则可以这样
Stonewt jest(9,12);
double best = 176.0;
Stonewt total;
total = jest + best; //best在后 jest是对象
等效于:
total = jes.operator+(best);//成员函数

但是只有友元函数
Stonewt jest(9,12);
double best = 176.0;
Stonewt total;
total = best + jest;
等效于:
total = operator+(jest, best);//友元函数
因此,建议加法重载建议定义为友元函数,可以让程序更容易适应自动类型转换

double量与object量相加:
1.友元函数,不用对象调用,两个参数,然后让接收单值的构造函数将double隐式转换为object类型
2.将加法运算符重载为一个显式使用double类型参数的函数, 相当于将隐式转换变成显示转换
object operator+(double x); //成员函数
friend object operator+(double x, object & s);

这样,
total = object + double与成员函数与 operator+(doublex)完全匹配,隐式转换,过程少,但是调用构造函数多,增加开销, 慢
total = double + object与友元函数operatpr(double x, object s);完全匹配,正好相反 快
经常需要double与object对象相加,重载加法,偶尔使用,则依赖自动转换,但是保险起见,显式转换为妙** P421

第十二章 类与动态内存分配

12.1动态内存和类
静态类成员:
用指针声明数组,可以在使用的时候给它分配空间。避免了预定义字符串的长度
静态存储类,无论创建了多少对象,程序都只创建一个静态变量副本。类的所有对象共享一个静态成员
就像家里的电话号码,全家使用一样

不能在类声明中初始化静态成员变量,因为声明只是告诉你如何分配内存,还没有实际内存分配
可以在类声明之外使用单独的语句进行初始化,因为它是单独存储的,不是具体对象的组成部分,只是用到的时候调用一下。声明需要static 但是初始化不需要
别在头文件中初始化静态成员变量,头文件可以到处有,可能会出现重复初始化。
注意:如果静态成员是整型或者枚举 const,则可以在声明中初始化。
相当于类对象之间共享的变量,可以统一管理同一个对象的所有类

因为指针的缘故,字符串并不是保存在对象之中,而是单独保存在堆内存中–new的缘故。对象仅保存了去哪查找字符串

析构函数调用时机
总结
1、在匿名对象使用完后立刻调用析构函数;对象生命周期结束
2、在栈区的对象,空间被释放后立刻调用析构函数;
3、在堆区的对象,空间被delete后调用析构函数;free不能调用;调用delete,会删除指针类对象。
4. 对象A是对象B的成员,B的析构函数被调用时,对象A的析构函数也会被调用

重点
P431–headline2导致析构函数调用的解释
类对象作为函数参数传递时,将会产生对象的副本,而这个副本是调用类中的拷贝构造函数实现的,如果类中没有自定义拷贝构造,编译器会自动生成一个缺省的函数,该函数有以下特点:
1、对于基本类型的成员变量,按字节复制
2、对于类类型的成员变量(成员子对象),将自动调用相应类的拷贝构造函数来初始化
3、指针只是简单的赋值,而不会自动分配内存。

当退出该函数时,副本将调用析构函数,删除该指针,指针所指向内存也已被释放。将原始对象只保留了一个指针值。当在类中释放对象再次调用
析构函数,会出现double free 的错误。
两种解决方式:
1、自定义拷贝构造函数。
2、将函数对象作为引用传递,引用时操作不会释放对象。

自动存储对象被删除的顺序与创建顺序相反。

classname object_1 = object;
初始化形式等效于:
classname object_1 = classname(object);
原型如下:
classname(const classname &); //编译器自动生成
被称为复制构造函数,因为它创建对象的一个副本,自动生成的构造函数不知道更新类对象共享
的静态变量。

12.1.2 特殊成员函数

C++自动提供以下成员函数–在没有人为给出定义的前提下
默认构造函数,默认析构函数,复制构造函数,赋值运算符,地址运算符
更准确的说,编译器将生成最后三个函数的定义,如果程序使用对象的方式要求这样做。例如,将一个对象赋给另外一个对象
另外两个特殊成员函数:移动构造函数,移动赋值运算符

默认构造函数
不做任何操作,不接受任何参数,因为创建对象时总会调用
classname::classname() {};//隐式默认构造函数,在一行
classname object ;//使得object类似于常规的自动变量,未初始化前值未知

如果定义了构造函数,默认构造函数将不会被调用,如果希望在创建对象时不显式地对它进行初始化,则必须显式地自定义默认构造函数,没有参数,但是可以使用它来设置特定的值
e.g.
classname::object() //显式默认构造函数
{
data = 0;
}

带参数的构造函数也可以是默认构造函数,只要参数值是提前知道的默认值
e.g.
object(int n = 0){ data = n;}//内敛构造函数,在一行
默认构造函数同一个类只能有一个

复制构造函数
将一个对象复制到新创建的对象中,应用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中,原型通常如下:
Class_name(const Class_name &);
接受一个指向类对象的常量引用作为参数:
e.g. StringBad(const StringBad &);

1.何时调用
新建一个对象,病将其初始化为同类现有对象时,如:
classname object_2(object_1);
classname object_2 = object_1;
classname object_2 = classname(object_1);
classname pt_object_2 = new classname(object_1);

中间的两种声明可能会使用复制构造函数直接创建对象,也可能生成一个临时对象,然后将临时对象的内容赋给要创建的对象,具体是哪一种方式取决于实现
最后一种是使用现有对象初始化一个
匿名对象*,并将新对象的地址赋给指针
每当程序生成了对象副本,编译器都将使用复制构造函数。如按值传递对象或者函数返回对象的时候(生成临时对象)。如三个对象相加,编译器将生成一个临时对象保存中间结果

因为复制构造函数占据空间,所以,引用传递对象是一个好的选择

2.功能 P434
默认的复制构造函数 逐个 复制 非静态成员(成员复制也称为浅复制),复制的是成员的
如果,类成员本身也是个对象,则将使用该成员对象的复制构造函数继续复制成员对象,
但是,静态函数不受影响,因为它属于整个类

(1)静态变量关于对象计数错误
默认的复制构造函数只管复制,但是不管创建了多少对象,不管更新统计关于对象数量的静态成员变量,但是析构函数任何对象过期都会调用,不管对象是否创建。
因此,如果类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题
e.g.
classname::object(const String & s)
{
num++; //更新静态成员变量
}
(2)乱码
因为隐式复制构造函数按值复制。例子中,sailor初始化sports后,得到的是两个指向同一个字符串的指针。operator<<() 函数使用指针来显式字符串,ok。但是析构函数会出现问题。析构函数释放了str指针指向的内存,但是这个指针指向的内存被sailor和sports里面连个指针同时指向。一个对象析构后清除内存,将会导致已经被析构释放内存再被释放一次,这将导致不确定,可能有害的结果。 或者程序直接终止
受损的字符串,通常为内存管理不善
解决方法–定义一个显式的复制构造函数
深度复制(deep copy)解决此问题。复制构造函数应当复制字符串,并将副本的地址赋给str(指针)成员而不是仅仅复制字符串的地址,这样每个对象都有了自己的字符串,而不是引用另一个对象的字符串。书本中的例子构造函数如下
StringBad::StringBad(const StringBad & st)
{
num_strings++;//更新静态成员变量
len = str.len;//同样的长度
str = new char(len + 1);//分配空间
std::strcpy(str,st.str);//复制
}
必须定义的原因是一些类成员使用new初始化、指向数据的指针,而不是数据的本身

赋值运算符
C++允许对象赋值,通过自动为类重载赋值运算符实现,原型如下:
Class_name & Class_name::operator=(const Class_name&);
接受并返回一个指向类对象的引用。与复制构造函数类似,赋值运算符隐式实现也是对成员逐个复制,如果成员为类,则调用该类的赋值运算符。静态数据不受影响。

将已有对象赋给另一个对象时,将使用重载的赋值运算符,直接等于的那种
P436
初始化总是会调用复制构造函数,使用=也可能会调用赋值运算符,在分两步处理新对象初始化的情况下。

对于由于默认赋值运算符不合适而导致的问题,解决办法是认为定义赋值运算符进行深度复制。类似复制构造函数,但有以下不同
1.由于目标对象可能引用了以前分配的数据,所以函数应使用delete释放这些数据
2.函数应当避免将对象赋值给自身,否则,给对象重新赋值前,释放内存的操作可能删除对象的内容
3.函数返回一个指向调用对象的引用,通过返回一个对象,函数可以像常规赋值操作那样,进行连续赋值。
e.g.
对象s1 = s2 = s3,函数表示法如下:
s0.operator=(s1.operator=(s2));

修正后的赋值运算符
SringBad & StringBad::operator=(const StringBad &)
{if(this == &st) // 自己赋值给自己return *this;delete []str; //删除旧的len = st.lenstr = new char(len + 1);//分配空间std::strcpy(str,st.str);//复制return *this;}

赋值操作不会创建新对象,不需要调整静态数据成员

12.2 改进后的新string类–自己编的
构造函数中 str = new char[1] 与str = new char; 分配的内存量相同,区别在于前者与析构函数兼容:delete [] str;后者与析构函数不兼容 delete[] 对应new[]
str = 0//指针str设置为0,即空指针 或者str = nullptr
等价于str = new char[1]; str[0] = ‘\0’

其他方式初始化的指针,delete[]结果不确定

[]中括号运算符 city[5], 二元运算符,一个操作数在第一个操作数之前,另外一个在两个括号之间
city–第一个操作数,5第二个操作数
P440
char & String::operator[] (int i)
{
return str[i];
}

之后 cout << opera[4]-----> couture << opera.operator[4]

const String answer(“haha”)
如果只有operator一种定义(此操作改变数据)下面这个操作将会报错
cout <<answer[1]; //answer 为常量
因为,无法保证数据不被修改
重载时,C++将区分常量和非常量函数的特征标,因此,可以额外提供一个仅const String对象可以使用的operator版本
const char & String::operator[] (int i) const
{
const str[i];
}

12.2.4 静态类成员函数

–公有,不与特定对象相关联
将成员函数声明为静态的(声明前加 static,独立的定义不加)将会导致
1.不能通过对象调用静态成员函数,静态成员函数也不可以使用this指针。
如果声明在公有部分,则可以使用类名和作用域解析运算符调用
e,g, string类有
static int howmany(){ return num_strings;}
调用方式:
int count = String::howmany();//调用了静态成员函数 类名 而不是对象名

2.只能使用静态数据成员,因为他们都是类公有,和特定对象无关
3.可以用来设置类标记(classwide)

12.2.5

优化:
String & String::operator=(const char *)
{delete [] str; //释放一下,之前可能用过了 len = std::strlen(s);str = new char[len + 1];std::strcpy(str,s);return *this;
}
P442String 输出例子函数
ostream & operator<<(ostream & os, const String & st)
{os << st.str;return os;
}String 输入 重载>>
istream & operator>>(istream & is, String & st)
{char temp[String::CINLIM]; //输入限制is.get(temp, String::CINLIM);if(is)st = temp;while(is && is.get()!= '\n')continue;return is;
}
较早的get(char *, int)版本读取空行后,返回的不是false,如果读入空行,字符串第一个字符是一个空字符
if( !cin || temp[0] = '\0')break;

如果使用了新的C++标准,!cin用作检测空行 ,第二个条件是旧版本的空行检测

12.3 构造函数中new的注意事项

空指针 C:0 或者NULL都可以,C++: 0 NULL nullptr,都可以,但nullptr更好
使用new初始化对象的指针的注意事项:
1.如果再构造函数中使用new初始化指针成员,则应在析构函数中使用delete
2.new和delete要互相兼容,new–delete,new[]–delete[]
3.如果有多个构造函数,必须以相同的方式使用new。因为只有一个析构函数。可以在一个构造函数中使用new初始化指针,在另一个构造函数中将指针初始化为空,因为,delete可以用于空指针
4.应该定义一个复制构造函数,通过深度赋值将一个对象初始化为另一个对象
这种构造函数与下方类似:
String::String(const String &st)
{
num_strings++;
len = st.len;
str = new char[len +1];
std;:strcpy(str,sst.str);
}
复制构造函数应该分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。同时更新所有受影响的静态类成员
5.应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象:
String & String::operator=(const String & st)
{
if(this == &st)
return *this; //对象自我复制,则退出
delete [] str; //清楚旧字符
len = st.len;
str = new char [len + 1];
std::strcpy(str, st,str);
return *this;
}

12.4 有关返回对象的说明

12.4.1 返回指向const对象的引用

返回对象将调用赋值构造函数,返回引用不会。
引用指向的对象,应该在调用函数执行前就已经存在
参数被声明为const,返回也必须const

12.4.2 返回指向非const对象的引用

常见的返回非const对象:重载赋值运算符,重载与cout<<运算符。前者为提高效率,后者是必须的

operator=() 用于连续赋值, 如s3 = s2, s2.operator=()返回值赋给s3, 则返回String对象或String对象的引用。
返回类型非const,则指向的对象可以修改
cout<< 最好引用

12.4.3 返回对象

当返回的对象是被调用函数的局部变量时应该返回对象本身,而不是指向对象的引用。因为被调用函数结束后,局部对象将会被释放

12.4.4 返回const对象

object_1+object_2 = object_3
object_1+object_2结果存储于临时对象中,然后 3 被赋给这个临时对象
释放的时候,临时对象被丢弃,原有对象不便,相当于啥都没干
将返回类型声明为const object时, obj+obj =obj 将是非法的

sum:
方法或者函数返回局部对象,则应返回对象,而不是指向对象的引用,这种情况下,使用复制构造函数来生成返回的对象
返回的是一个没有公有复制构造函数的类的对象的话,则必须返回一个指向这种对象的引用
最后,有些方法或函数(如重载赋值运算符)可以返回对象,也可以返回指向对象的引用,首选引用,高效

12.5 使用指向对象的指针

String * p = new String(sayings[choice]);
p 指向new 新创建的未被命名的对象,使用对象saying[choice]来初始化新的String对象,将调用复制构造函数。

使用enw初始化对象
如果,Class_name是类,value的类型是Type_name,则语句:
Class_name * pclass = new Class_name(value);
将调用构造函数:
Class_name(Type_name);
这里可能需要转换,如 Class_name(const Type_name &);
P453
Class_name * ptr = new Class_name, 将调用默认构造函数

12.5.1 再谈new new和delete
class act {…};
act nice;//外部对象(静态对象)
int main()
{
act p = new act;//动态对象
{
act up;//自动对象
} //此时,将调用自动对应up的析构函数
delete p;//对指针p应用运算符delete时,将调用动态对象
p的析构函数
} 整个程序结束时,将调用静态对象niec的析构函数

delete 只释放保存p指针的空间,不释放p指向的内存,释放内存由析构函数完成析构函数调用时机:
1.变量是动态变量,执行完定义该对象的程序块时
2.静态变量(外部,静态,静态外部或来自名称空间),程序结束时
3.new创建的,仅当显式使用delete删除该对象

12.5.2 小结P454 指针与对象小结

12.5.3 再谈定位new运算符

1.定位new运算符重复在同一个buffer为不同的对象分配内存是危险的————
程序员得自己手动管理,不同对象,不同的内存单元,提供不同的缓冲区buffer地址
e.g.
pc1 = new(buffer) justtesting;
pc3 = new(buf + sizeof(justtesting)) justtesting(…); //第二个设定偏移量

2.如果使用定位new运算符来为对象分配内存,必须确其析构函数被调用。
delete 不能与定位new运算符配合使用
内存单元buffer数组里面的对象在delete[]buffer时,不会调用里面对象对应的析构函数
bufferGG了,但是里面的对象还在

解决方法:显式地为使用定位new运算符创建的对象调用析构函数。这是需要显式调用析构函数的少数集中情况之一。显式调用析构函数,必须制定要销毁的对象。
pc3->~justtest();//point-> ~func()

对于定位new运算符创建的对象,应该使用与创建顺序星饭的顺序进行删除,因为晚创建的对象可能依赖于较早创建的对象,另外,仅当所有的对象销毁后,才可以释放对应的缓冲区buffer    直接new的对象,在堆中,因此释放方法:delete pc2;//释放pc2指向的对象

12.6 技术总结P459

12.7 应用
(例子的声明再私有部分)类声明中结构定义在一行,可以使其作用域为整个类,struct结构作为一个类型,声明其他类成员或者作为类中其他类方法的类名名称,但只能在类中使用。—类嵌套结构。此时的结构声明不会创建数据对象,知识制定了可以在类中使用的类型,如果声明位于私有部分,则只能在这个类中使用。如果在公有部分,则可以从类的外部通过作用域解析符使用被声明的类型。

const初始化
一般来说,调用构造函数时,对象将在括号中的代码执行之前被创建。即先给成员变量分配内存,然后进入括号内,将值存储到内存中。const成员,必须在执行到构造函数体之前,即创建对象时进行初始化

成员初始化列表:

e.g.
Queue::Queue(int qs) : qsize(qs), mdata(value), …
//将qsize初始化为qs ,qsize为const
{

}
只有构造函数可以使用这种初始化列表语法
除了const类成员,被声明为引用的类成员也必须使用这种语法简单类成员,成员初始化列表和函数体内使用赋值没区别
class agency {…};
class agent
{
private:
agency & belong; //必须使用初始化列表来初始化

};
agent::agent(agency & a) : belong(a) {…}
上方这个是agent的构造函数,传入了a的agency类引用,赋给belong

成员初始化列表的语法:
Classy是一个类,mem1、mem2、和mem3是它的数据成员,可以使用以下语法来初始化
Classy::Classy(int n, int m): mem1(n), mem2(0), mem3(n*m + 2)
{

}
在创建对象时完成,此时括号内代码还没有执行
注意:
1.这种格式只能用于构造函数
2.必须使用这种格式来初始化非静态const成员
3.必须使用这种格式初始化引用数据成员

类初始化
C++11可以这么做
class Classy
{
int mem1 = 10;
const int mem2 = 29;

};
与构造函数使用成员初始化列表等价:
Classy::Classy() : mem1(10), mem2(29) {…}
以上是默认构造函数,构造函数也可以这么做,这将覆盖默认构造函数初始化列表的值

向队列中添加节点使用new,需要显示析构函数删除到期的所有结点。

使用new的类通常需要包含 显式复制构造函数 和执行深度复制的 赋值运算符。定义赋值运算符进行深度复制。类似复制构造函数

要克隆或复制队列,必须提供以上两个函数。复制对象将会生成一个和原来一模一样的,指针也是,这将导致不必要的修改发生。
方法:将所需的方法定义为 伪私有方法
class Queue
{
private:
Queue(const Queue &q) :qsize(0) {}
Queue & operator=(const Queue & q) { return *this};

} 重点 待解决 P468
作用:1.避免了本来将自动生成的默认方法定义,2.因为方法私有,所以不会被广发使用,如果nip和truck是Queue对象,则以下编译器不通过
Queue snick(nip);
truck = nip;
提供编译错误,指出某一些方法不可使用

在没有提供深度复制运算符和显式复制构造函数的的、具体定义代码的情况下,程序可能仍然能运行,
但是结果不确定。为了防止这种情况下,程序不崩溃,将所需的方法定义为伪私有方法:
class Queue
{
private:
Queue(const Queue & q) : qsize(0) {}
Queue & operator=(const Queue & q) { reuturn *this}

}
这可以避免编译器自动给你生成临时的定义,也可以避免方法被广泛使用
nip 和truck都是Queue的对象
Queue fun(nip)和truck = nip 都会编译报错
这样可以避免未来因为未提供上两个函数定义而突然报错。如果未来定义该类不可复制,也可使用这种方法

第十三章 类继承

class 内单行的内联函数定义,后可加 ; 可不加
函数库
类库 = 类声明+类实现 以源代码的方式提供
继承:
可以在已有类的基础上添加功能,给类添加数据,修改类方法。
在只有类方法头文件和编译后代码的情况下,仍然可以使用库中的类派生出新的类,同时在不公开实现的情况下将自己的类分给别人,同时允许他们也添加新特性

13.1 一个简单的基类
一个类派生出另一个类,原始类–基类,继承类–派生类
class class_name_1 : public class_name;
{

}
class_name是一个公有基类—公有派生
派生类对象也包含基类对象。
使用公有派生基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分
,但只能通过基类的公有和保护方法访问,继承类无法直接访问。

新的class_name_1类对象有以下特征: P483图
1.派生类对象存储了基类的数据成员(派生类继承了基类的实现)
2.派生类对象可以使用基类的方法(派生类继承了基类东街口)

需要在继承特性中添加:
1.派生类需要自己的构造函数
2.派生类可以根据需要添加额外的数据成员和成员函数
构造函数必须给新成员(如果有)和继承的成员提供数据

????提供了默认参数的默认构造函数怎么弄
—默认构造函数不带参数,构造函数可以设置默认值代替默认构造函数

13.1.2 构造函数:访问权限
派生类不能直接访问基类的私有成员,必须通过基类公有方法进行访问基类的
私有成员。即:派生类构造函数必须使用基类构造函数

创建派生类对象时,程序首先创建基类对象。即基类对象应当在程序进入派生类构造函数之前被创建
C++使用成员初始化列表完成该工作
e.g.
RatedPlayer::RatedPlayer(unsigned int r, const sring & fn, const string & In, bool ht)
: Table(fn, In, bt)
{
rating = r;
}
其中,Table(fn, In, bt)是成员初始化列表,它执行的代码调用Table构造函数。
如:

RatePlayer rplayer1(1140,"haha", "ff", true);
RatePlayer 构造函数将实参"haha", "ff", true 赋给形参 fn, In, ht,然后将这些参数作为实
参传递给Table构造函数,后者将创建一个嵌套Table对象,并将数据   存储在该对象中,然后,程序进入
RatePlayer构造函数中,完成RatePlayer对象的创建,并将r赋给rating成员

如果不提供成员初始化列表,
RatedPlayer::RatedPlayer(unsigned int r, const sring & fn, const string & In, bool ht)
{
rating = r;
}
则与
RatedPlayer::RatedPlayer(unsigned int r, const sring & fn, const string & In, bool ht) : Table()
{
rating = r;
}
等效,相当于调用了Table 的默认构造函数,除非调用默认构造函数,否则请显式调用正确的基类构造函数
RatedPlayer::RatedPlayer(unsigned int r, const Table & tp) : Table(tp)
{
rating = r;
}
tp的类型为Table &,因此将调用基类的复制构造函数,如果此时,基类没有定义构造函数,编译器给他 自动生成一个,此时,动用隐式复制构造函数合适,因为没有动态分配内存

可以对派生类成员使用成员初始化列表,此时应使用成员名而不是类名。
RatedPlayer::RatedPlayer(unsigned int r, const sring & fn, const string & In, bool ht) : Table(), rating®
{
}

要点:
1.首先创建基类
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
3.派生类构造函数应初始化派生类新增的数据成员

析构时,先派生类,后基类
P486
除了虚基类,类智能将值传递回相邻的基类

13.1.4 派生类和基类之间的特殊关系

1.派生类可以使用基类非私有的方法
2.基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显示类型转换的情况下引用派生类对象
e.g.
RatedPlayer rplayer1(…);
Table & rt = rplayer;
Table * pt = & rplayer;
rt,name();
pt->name(); //以上这些操作只能基类指向派生类
但是也只能用于调用基类方法,无法调用派生类的方法,派生类新增的方法无法调用,因为派生类继承了基类的方法,转半天相当于调用了自己的方法。
C++要求引用和指针类型与赋给的类型匹配,但对于继承例外。但也只能将派生类对象和地址赋给基类引用和指针,反之,派生类通过基类调用派生类方法,error,基类没有派生类方法

P489 真鸡儿绕 重点待解决

13.2 继承:is-a关系

三种方式:
私有继承
保护继承
公有继承:最常见,建立is-a关系,即派生类对象也是一个基类对象,可以对基类对象做的任何事,在派生类身上也可以做,又因为派生类通常会添加新特性,所以用is-a-kind-of描述这种关系更准确
公有继承不建立has-a关系
has-a:有一个,聚合关系,A有一个B。某个对象是另一个对象的一部分。
not-a:现实里有关系,但是代码层面没联系。避免不必要的继承是先给出大概的设计,为类和派生类写出属性和行为,如果某一个类没有特定的属性或方法,或所有属性和方法都被派生类重写,都应该重新设计。
公有继承不建立is-like-a关系,也即是说不采用明喻。例如,律师像鲨鱼,其实并不是。所以不应从shark类派生出Lawyer类。继承可以在基类的基础上添加属性,但是不能删除属性。有时,可以设计一个包含共有特征的类,然后以is-a或者has-a关系在这个类基础上定义相关类。
公有继承不建立is-implemented-as-a关系(作为…实现)。例如,可以通过数组来实现栈,但从array类派生出的stack类是不是适合的。因为栈不是数组。正确的方法是,让栈包含一个私有array对象成员来隐藏数组实现
公有继承不建立use-a关系例如,计算机可以使用激光打印机。但computer类派生出printer类(或反过来)是没意义的。然而,可以使用友元函数或类处理printer对象和computer对象之间的通信

    C++ 可以通过公有继承来建立has-a,is-implemeted,或uses-a关系。坚持is-a最好

13.3多态公有继承–虚函数

希望从基类继承而来的派生类的方法与基类有区别——方法的行为取决于调用该方法的对象
——多态——具有多种形态,即同一种方法的行为随上下文而异。两种方法:
1.在派生类中重新定义基类的方法-覆盖基类方法
2.使用虚函数

is-a 关系通常是不可逆的,也就是说 水果不是香蕉
如果方法是通过引用或者指针而不是对象调用,它将确定使用哪一种方法。如果没有关键字virtual,程序将根据引用类型或者指针类型来选择方法。如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。(派生类引用无法直接指向基类)
没有virtual,引用或指针的类型
Class A;
Class B : public A;
A a;
B b;
A & re_a = a;
A & re_b = b;
re_a.view(); // use A::view()
re_b.view();// use A::view()
使用virtual,引用或指针指向的对象的类型
re_a.view(); // use A::view()
re_b.view();// use B::view()
在基类中将派生类会重新定义的方法声明为虚方法,派生类同名的方法将自动成为虚方法,但是在派生类声明中使用virtual更显眼。
基类中声明了一个虚析构函数,这将确保释放派生类对象时,按照正确的顺序调用析构函数,即先调用派生类析构函数,再自动调用基类析构函数
此时这个虚析构函数不能执行任何操作,如果派生类中的析构函数

    虚函数定义代码中virtual不要加        派生类不能直接访问基类的私有数据,必须使用基类的公有方法才能访问

派生类构造函数在初始化基类私有数据时,采用的时成员初始化列表语法,将基类信息传递给基类构造函数,然后使用基类构造函数初始化基类。
非构造函数不能使用成员初始化列表语法。但是派生类可以调用基类的公有的基类方法,只需要加上基类名和作用域解析运算符

数组的元素类型必须相同,因此无法存储不同的类对象。但是在基类和派生类中,因为时公有继承,具有is-a的关系,基类指针可以指向派生类,所以可以创建指向基类的指针数组,这样就保证了类型的相同,同时也可以通过基类的指针指向了派生类对象。–使用一个数组来表示多种类型的对象,这就是多态性
当然,此时因为virtual虚函数,同名方法不同实现,且时指针或者引用调用了方法,因此这里实现了虚函数的应用
如果析构函数不是虚的,则只调用对应于指针类型的析构函数(基类),即使实际上它指向的时派生类。
如果析构函数时虚的,则将调用相应对象的析构函数,顺道调用基类的析构函数
如果派生类中自己声明了一个执行某些操作的析构函数,基类中就必须声明一个 虚析构函数 即使该函数啥都不做

13.4 静态联编和动态联编

编译器决定程序调用函数时,执行哪个代码块–函数名联编。
编译过程中进行联编–静态联编 static bingding/早期联编 early binding
编译过程发生在程序运行时动态联编 dynamic bingding/晚期联编 late binding
C++中,动态联编与通过指针和引用调用方法有关–由继承控制,
C++通常不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种引用类型
然而,基类可以,不必进行显式类型转换
将派生类指针或引用转换为基类指针或引用–向上强制转换,upcasting这使得公有继承不需要进行显式类型转换,该规则是is-a关系的一部分
因为派生类继承了所有基类的成员,所以为基类设计的函数也可以对派生类执行同样的操作。向上强制转换具有传递性。基类指针可以无限向下延申

相反,将基类指针或引用转换为派生类指针或引用–向下强制转换,downcasting如果不使用显式类型转换,向下强制类型转换是不允许的。因为is-a关系通常不可逆。因为派生类具有基类所不具有的成员。
以上说人话就是基类指针或引用可以无条件指向其派生类,反之不建议,且需要强制转换
编译器对非虚方法使用静态联编。虚方法使用动态联编

动态联编需要不断跟踪对象,在不需要的时候,静态联编效率更高,仅用到虚函数的时候再使用动态联编
P504–虚函数的编译器实现
给每个对象添加一个一藏匿成员——它保存了一个指向函数地址数组的指针
这个数组叫做:虚函数表/virtual function lable,vtbl
虚函数表中存储着类中声明的虚函数的地址。同时,每个类都有自己独立的虚函数表
派生类如果虚函数有了新定义,则相应的保存新定义的虚函数的地址,如果没有,则保存原始版本的地址,如果派生类增加了新的虚函数,也将被添加到虚函数表中。
调用虚函数时,通过vtbl指针找到虚函数表,再找到相应的虚函数。

13.4.3 有关虚函数的注意事项
1.基类方法中使用virtual关键字,可以使得它在基类和相应的派生类中是虚函数
2.使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不是为引用或指针类型定义的方法–动态联编,这样基类指针或引用可以指向派生类对象
3.应该在基类中将那些可能会在派生类中 充型定义的方法声明为虚的。

构造函数
不能为虚的,派生类有自己的构造函数,同时通过成员初始化列表,将基类信息传递给基类构造函数进行相应创建。
析构函数
应当是虚函数,除非类不用做基类。但是给非基类定义了一个虚析构函数也没啥影响
友元
不能是虚函数,因为它不是类成员,只有类成员才能是虚函数
没有重新定义
如果派生类没有重新定义函数,则使用基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。除非基类版本隐藏
重新定义将隐藏原有方法
在派生类中重新定义方法,将对派生类隐藏基类中同名方法,而不是重载
(overwrite & override)
经验规则: leads to a couple rules of thumb
1.重新定义继承的方法,需要确保原型完全相同基类指针/引用可以指向派生类,并调用与基类同型方法,不能调用派生类中异型方法。不同型时,则再次根据引用或指针类型来调用,此时virtual也就没意义
但如果返回类型 是基类引用或指针,则可以修改为指向派生类的引用或指针只适用于返回值,不适用于参数
——返回类型协变/covariance of return type, 因为允许返回类型随着类的类型比那花而变化。

2. 如果基**类声明被重载了**,则应该在**派生类中重新定义所有的基类版本**。e.g.class a{public://类中被重载virtual void haha(int a) const;virtual void haha(double a) const;virtual void haha() const;}class b : public a{public://因为基类中已经发生中重载,因此派生类中需要**全部**重定义virtual void haha(int a) const;virtual void haha(double a) const;virtual void haha() const;}**如果只重定义一个版本,则另外两个版本将被隐藏**,派生类对象无法使用他们。当然,如果没有修改的需求,则新定义可只调用基类版本

13.5 访问控制:protected
与private类似,区别在于:派生类成员可以直接访问基类的保护成员,但不能直接方位基类的私有成员。
因此对于外部来说,保护成员的l行为与私有成员类似。但是对于派生类来说,保护成员的行为与公有成员类似。对于成员函数来说,可以让派生类访l问外l部不能使用的内部函数。但是l成员变量最好还是定义为i为私有的,
13.6 抽象基类 abstract bsse class ABC
circle是特殊的ellipse(椭圆),但是定义园的时候,如果从ellipse派生,将有大量信息是circle所不需要的。因此可以从circle 和ellipse 中提取出abc,然后从abc分别派生出circle和ellipse,这样就可以使用基类指针管理circle和ellipse对象–即多态的方法。对于面积来说,circle 和ellipse 计算方式不同,Area()实现方式不同。
高度抽象下,ABC不具备 area()需要的数据成员
C++通过使用纯虚函数-pure virtual function提供未实现的函数。纯虚函数声明的结尾处为 =0
e.g.
class BaseEllipse //抽象基类
{
private:
double x;
double y;

public:
BaseEllipse(double x0 = 0, double y0 = 0) : x(x0),y(y0) {} //emmm,真是兰
virtual ~BaseEllipse {}
void Move(int nx, ny) {x = nx; y = ny;}
virtual double Area() const = 0; // 纯虚函数

}
当类声明中包含纯虚函数,则该类不能创建对象,因为纯虚函数的实现用到了不存在的变量
只能用作基类。原型中 =0 使得虚函数成为纯虚函数。

C++甚至允许纯虚函数有定义的代码。例如Move(),虽然没必要,但你仍然可以将其声明为虚函数
void Move(int nx, ny) = 0; 这将使得基类成为抽象的。但仍可以在实现文件中提供方法的定义
void BaseEllipse::Move(int nx, ny) {x = nx; y = ny;}
=0指出类是一个抽象类,在类中可以不定义函数。
针对抽象类BaseEllipse来说 ,circle 和ellipse 就是具体类 concrete。

总之,ABC至少有一个使用纯虚函数的接口,从ABC派生处的类将根据派生类的具体特征,使用常规虚函数来实现这种接口 P510

13.7 继承和动态内存分配
13.7.1 第一种情况,派生类不用new
基类使用动态内存分配,P516,声明中包含构造函数使用new时所需要的特殊方法:
析构函数,复制构造函数,重载赋值运算符
class baseDMA
{
private:

public:
baseDMA(const char * l = “null”, int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA &rs);
};
从基类派生到lackDMA,派生出来的不使用new,无特殊特性。不需要显式定义析构函数,复制构造函数和赋值运算符。
class lackDMA : public baseDMA
{

};
分析
析构函数
如果没有定义析构函数,编译器会自己定义一个不执行任何操作的默认析构函数。派生类的默认析构函数执行自己的代码之后会调用基类的析构函数,
——因为,不需要执行任何特殊操作的派生类,编译器构造的默认析构函数就足够了,它自己会去调用基类的析构函数。
复制构造函数
默认复制构造函数执行成员复制,对于动态内存分配不合适。但是对于不需要动态内存分配的派生类来说是合适的。对于类对象来说。复制派生类对象时,派生类的用默认复制构造函数将使用显式的基类复制构造函数来复制派生类对象的基类部分。因此默认复制构造函数对于新的派生类对象成员来说很合适,对于继承的基类对象来说也ok
赋值:
派生类的赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。

第二种情况:派生类使用new
class hasDMA : public baseDMA
{
};
必须为派生类定义显式析构函数,复制构造函数和赋值运算符
析构函数:
派生类的析构函数自动调用基类的析构函数,故自身的职责是对派生类构造函数做过的动作进行清理。即,如果派生类有了自己私有指针或new出来的东西,只能使用自己的析构函数清楚,和基类一样的数据就交给基类析构函数。

复制构造函数:
派生类的复制构造函数只能访问派生类的数据,因此必须调用基类复制构造函数来处理共享的基类数据
baseDMA::baseDMA(const baseDMA & rs);
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) //成员初始化列表
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
成员初始化列表将一个hasDMA引用传递给baseDMA构造函数,
虽然baseDMA(基类)需要一个baseDMA的引用,但是因为基类引用可以指向派生类,因此,基类复制构造函数可以使用派生类中的基类部分来构造新派生类中的基类部分。而不需要实际上的给它一个基类引用。
赋值运算符:
一般模式baseDMA的
baseDMA & baseDMA ::operator=(const baseDMA & rs)
{
if(this == &rs)
return *this //自己等于自己了
delete [] label;
label = new char[ste::strlen(re.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
};
此时的派生类hasDMA使用了动态内存分配,所以需要一个显示赋值运算符。虽然它只能直接访问派生类的数据,但是派生类的显示赋值运算符必须负责所有继承的baseDMA基类对象的赋值,可以通过显式调用基类赋值运算符来完成该工作
hasDMA & hasDMA ::operator=(const hasDMA & rs)
{
if(this == &rs)
return *this //自己等于自己了
baseDMA::operator=(hs); //复制基类部分 含义是 *this = hs; 使用了基类复制运算符复制派生类中基类的部分
delete [] label; //准备一个新类
label = new char[ste::strlen(re.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数,复制构造函数和赋值运算符都必须使用相应的基类方法来处理基类元素。
对于析构函数,自动完成。

对于构造函数,将通过初始化成员列表中调用基类的复制构造函数来完成,如果不这样做,将自动调用基类的默认构造函数。
对于赋值运算符,通过使用作用域解析运算符显式地调用基类的赋值运算符来完成。

13.7.3 P519示例
P522
友元函数不是成员函数,不能使用作用域解析运算符指定哪个(当前派生类的函数还是基类中的函数)成员函数来访问基类中的成员数据,因此可以通过强制类型转换,将派生类转换为基类,以便匹配原型时找到正确的函数,
e.g.
std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
// type cast to match operator<<(ostream &, const baseDMA &)
os << (const baseDMA &) hs;
os << "Style: " << s,style << endl;
return os;
}

13.8 小结

13.8.1 编译器生成的成员函数
1.默认构造函数
要么没参数,要么都有默认值——状态确定的参数列表。在没有定义任何构造函数,编译器将定义默认构造函数。
e,g,
Star rigel;//创造了一个没有显式初始化的对象,需要默认构造函数。
自动生成的默认构造函数另一个功能:是可以调用基类的默认构造函数以及调用本身是其他类成员时,该类的默认构造函数。
另外,如果派生类构造函数的成员初始化列表没有显式调用基类构造函数。则编译器将使用基类的默认构造函数(此时,的默认构造函数时人为提供的)来构造派生类对象的基类部分,
这种情况下,如果基类没有构造函数,将编译错误。如果定义了某种构造函数,编译器将不会定义默认构造函数,这种情况下,如果需要默认构造函数,必须自己提供。

    提供构造函数是为了保证对象能被正确的初始化,包含指针成员,必须初始化。最好提供一个显式默认构造函数,将所有类数据成员初始化为合理的值

2.复制构造函数
接受同类对象作为参数。使用场景:
将新对象初始化为同类对象
按值传递给函数
按值返回对象
编译器生成临时对象
如果程序没有使用(不论显式还是隐式)复制构造函数,编译器将只提供原型,但不提供定义(相当于没用)。(如果用到的话)否则,程序将定义一个执行成员初始化的复制构造函数,即,新对象的每个成员都被初始化为原始对象相应成员一样的值。如果成员为类对象,则初始化成员时,将使用相应类的复制构造函数
某些情况下,如new初始化成员指针,必须进行深度复制。
e.g.
//深度复制
baseDMA::baseDMA(cosnt baseDMA & re)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label)
rating = rs.rating;
}
3.赋值运算符
默认的赋值运算符处理同类对象之间赋值了——已有对象之间。
如果需要显式定义复制构造函数,基于相同的原因,需要显式定义赋值运算符
编译器不会生成将一种类型赋给另一种类型的赋值运算符,如果希望那么干,一,
Star & Star::operator=(const char *) {…}-----快
也可以使用转换函数,将字符串转换为Star对象,然后将Star赋给Star的赋值函数–可能导致编译器混乱
18章:移动构造函数,移动赋值运算符

13.8.2 其他类方法
1.构造函数
构造函数创建新对象,其他类方法只是使用先用类,因此构造函数不被基层的原因之一。继承意味着派生类对象可以使用基类的方法。
2.析构函数
一定要显式定义析构函数来释放使用new分配的内存,并完成任何特殊清理工作。对于基类,即使不需要,也清提供虚析构函数
或者,类中包含需要独立修改的静态变量,这需要自定义复制构造函数
3.转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换
e.g. Star(const Spectral &, int members = 1); //将Spectral转化为Star
将可转换的类型传递给以类参数的函数时,将调用转换构造函数
e,g,
Star north;
north = “polaris”;
第二句调用Star::operator=(const Star )函数,首先将char字符串生成了一个Star对象,该对象之后被用作赋值运算符的参数(这里假设没有定义将char赋给Star的赋值运算符)
e,g,
class Star
{

explicit Star(cosnt char *);

};

Star north;
north = “polaris”; //not alowed
north = Star(“polaris”); //allowed
}
将类对象转换为其他类型,就需要定义转换函数了(第十一章)
转换函数可以是没参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数,即使没有声明返回类型,函数也因该返回所需的转换值
e.g.
Star::Star double() {…}
Star::Star const char *() {…}
4.按值传递对象与传递引用
为了效率,按引用传递。
按值传递设计生成临时拷贝:调用复制构造函数,析构函数。
不修改对象的情况下,const引用更好
继承使用虚函数,被定义为接受基类引用参数的函数可以接受派生类
5.返回对象和返回引用
如果可以不返回对象,返回引用更好
代码方面,返回对象和返回引用的区别在于引用多 了个 & 符号,其他没区别
返回对象也涉及到返回对象的临时副本。 函数不能返回函数内的临时对象的引用。
函数返回的是函数内临时对象,则不返回引用。函数返回的是通过引用或指针传递给他的对象,返回引用
6.const使用
参数列表内 保证不修改参数, 函数名后面,不修改调用它的对象
函数名前,返回的东西不能用于修改对象中的数据
e.g.
const Stock & Stock::topval(coanst Stock & s) const
{
if(s.total_val > total_val)
return s; // 参数对象
else
return *this; //调用方法的对象
}
该方法返回对this或s的引用。都被声明为const ,所以函数不能修改他们,意味着返回引用也必须被声明为const
如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也不会修改该参数

13.8.3 公有继承
1.is-a关系
如果派生类不是一个特殊的基类,不要使用公有派生。
某些情况下,可以创建包含纯虚函数的抽象数据类,从它派生其他的类。
is-a关系无需进行显式类型转换。基类指针可以指向派生类对象,基类引用可以引用派生类对象。反过来就不行了,这时候就需要将派生类显示强制转换为基类。
2.不能被继承的东西
构造函数不能被继承。创建派生类必须调用派生类的构造函数,派生类构造函数通过成员初始化列表语法调用基类构造函数创建派生类对象中的基类部分。如果没有显式调用基类构造函数,将使用基类的默认构造函数。
析构函数也不能被继承。但是析构函数首先作用与派生类
赋值运算符不能被继承,它的特征标随类而异
3.赋值运算符
编译器发现一个对象赋给另一个对象,自动为这个类提供赋值运算符,这个运算符的默认或隐式版本将采用成员赋值,即将原对象相应成员赋给目标对象的每一个成员。 然而,如果对象属于派生类,编译器将使用基类赋值运算符来处理派生类对象中的基类部分的赋值,
如果显式地为基类提供赋值运算符,将使用它,
当然,成员是另一个类的对象,将使用对应的赋值运算符。
对于派生类的基类部分,C++使用基类的赋值运算符,不需要派生类重新定义赋值运算符,除非添加了需要特别留意的数据成员。例如baseDMA类显式地定义了赋值,但是派生类lackDMA使用了它生成的隐式赋值运算符
如果,派生类构造函数使用了new初始化指针,则需要提供一个显式赋值运算符。类的每个成员都必须有自己的赋值运算符。
hasDMA & hasDMA::operator=(const baseDMA & hs)
{
if(this == &hs)
return *this;
baseDMA::operator=(hs); //赋值基类部分
delete [] style; //prepare for new style
style = new char[std::strlen(hs.strlen) + 1];
std::srcpy(style, hs.style);
return *this;
}
当派生类对象赋给基类对象时,赋值语句将被转换成左边对象调用自己的赋值运算符,参数是派生类。
e.g.
base = derived;
—>base::operator=(const base &)
is-a关系允许base指向derived,该函数只能处理基类成员。
总之,可以那么干,但只涉及到基类成员。
相反 derived = base 将调用 derived::operator=(const derived &)。但派生类不能指向基类,error
除非,有derived(const base &)----转换构造函数
两个函数合作,首先将base转换为一个derived的临时对象,然后将他用作赋值运算符的参数

    **另一个方法是定义一个将基类赋给派生类的赋值运算符**derived * derived::operator=(const base &) {...}

4.私有成员和保护乘员
派生类而言,保护成员类似公有成员;但是对外部而言,保护成语与私有成员类似。派生类可以直接访问基类的保护乘员。
5.虚方法
设计基类时,必须确认方法是否需要声明为虚的。如果希望派生类能重定义方法,则定义为虚。这样可以启用动态联编。
6.析构函数
基类的因当是虚的。这样当通过指向对象的基类指针或引用可删除派生类对象和基类,而不仅仅删除基类
7.友元函数
友元函数非成员函数,不能继承,可以通过** **
第十五章 dynamic_cast<>也可以,更好
8.基类使用方法
P529

C++ Primer Plus- 类与对象-note2相关推荐

  1. C++ Primer Plus学习(十)——类和对象

    类和对象 抽象和类 构造函数和析构函数 this指针 对象数组 类作用域 运算符重载 友元 类的自动转换和强制类型转换 抽象和类 指定基本类型完成了三项工作: 决定数据对象需要的内存数量: 决定如何解 ...

  2. JAVA入门第二季 第一章 类和对象

    面向对象编程 Object Oriented Programming OOP 第一.什么是类和对象 在具体说明类和对象之前,先说说别的. 眼睛在人类身体上最为有用的器官.如果一个没有了眼睛,这个人与世 ...

  3. 【C++】类和对象---什么是类?

    目录 1.面向过程和面向对象初步认识 2.类的引入 2.1使用struct定义类 3.类的定义 3.1类的两种定义方式: 3.2成员变量命名规则的建议 3.3成员函数与成员变量定义的位置建议 4.类的 ...

  4. C++ Primer 与“类”有关的注意事项总结

    C++ 与"类"有关的注意事项总结(一) 1. 除了静态 static 数据成员外,数据成员不能在类体中被显式地初始化. 例如 : class First { int memi = ...

  5. 快学Scala学习笔记及习题解答(5-8类、对象、包和继承)

    本文scala使用的版本是2.11.7 第五章 类 5.1 基本操作 class Person {// Scala会生成一个私有的final字段和一个getter方法,但没有setterval tim ...

  6. C++ 笔记(18)— 类和对象(this 指针、指向类的指针、类静态成员变量和函数)

    1. this 指针 在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址. this 指针是所有成员函数的隐含参数.因此,在成员函数内部,它可以用来指向调用对象. 友元函数没有 thi ...

  7. C++ 笔记(16)— 类和对象(类定义、类实例对象定义、访问类成员、类成员函数、类 public/private/protected 成员、类对象引用和指针)

    1. 类的定义 类定义是以关键字 class 开头,后跟类的名称.并在它后面依次包含类名,一组放在 {} 内的成员属性和成员函数,以及结尾的分号. 类声明将类本身及其属性告诉编译器.类声明本身并不能改 ...

  8. C02-程序设计基础提高班(C++)第9周上机任务-类和对象

    第9周:阅读教材第8章(p231-262),主要内容是类和对象,学会定义类和对象解决问题,完成第9周上机任务: (回到C02-程序设计基础提高班(C++)学习安排) 实践任务: [任务1]阅读.运行下 ...

  9. 函数返回类的对象与拷贝构造函数

    C++中,如果我们在一个函数中,定义了一个类的对象,然后返回这个对象,在main函数中用一个对象去接受这个返回的对象的时候,这里面参与的函数调用大家可能不熟悉,这里通过程序和注释的方式给大家讲解一下. ...

最新文章

  1. shell中$XX相关
  2. 2021北京高考英语口试成绩查询,2021北京高考英语口语怎么考?2021英语增加口语考试是全国?...
  3. 升级Python2.7后 no module name yum
  4. 95-140-108-源码-transform-算子process
  5. Boost.Bind的基础使用
  6. Jvm虚拟机下载和安装教程
  7. HTML5 汉字上方添加拼音标注 ruby、rp、rt
  8. 大数据可视化工具-大屏展示
  9. pdf文件如何生成目录 wps_WPS中如何自动生成目录
  10. Open JDK patched with font fix
  11. java xss漏洞修复_全局存储型XSS漏洞修复
  12. python histogram函数_Python numpy.histogram_bin_edges函数方法的使用
  13. 硕士期间两篇计算机sci二区,实验室两篇论文被SCI 2区期刊Neurocomputing接收
  14. JDO与JPA哪个更好?
  15. Python基于PHP+MySQL的个人网页设计与实现
  16. Python这么强大, 怎样快速学习?
  17. 【微机原理】数字电路器件—门 与门 或门 非门电路及实例
  18. 【教育小程序案例】线下培训机构辅导教育
  19. 使用AWS最便宜的GPU实例  from 动手学深度学习v2 李沐大神
  20. Java写时复制CopyOnWriteArrayList

热门文章

  1. NYOJ 455题 黑色帽子
  2. 《弃子长安》第七章 真相迷离
  3. 第七节:面向对象——练习
  4. 高考数学知识点:数列压轴小题秒杀技巧
  5. 怎么查看浏览器保存的密码?
  6. 龙之信条服务器维护,《龙之信条》终于通关了,说点大家感兴趣的问题吧
  7. linux struts2 乱码,遇到的Struts2文件下载乱码问题
  8. 5s邮箱怎么总是连不上服务器,iphone5s的手机邮箱应该怎么设置
  9. Python pandas.DataFrame.round函数方法的使用
  10. 九年级学生学计算机,励志的句子初三学生