第十天
1.复制构造函数
只有单个形参,该形参是对本类类型对象的引用(常用const修饰)。
- 根据另一个同类型的对象显式或隐式初始化一个对象。
- 复制一个对象,传递给一个函数。
- 从函数返回是复制一个对象。
- 初始化顺序容器中的元素。
- 根据元素初始化列表初始化数组元素。
初始化的两种形式:直接初始化和复制初始化。
直接初始化直接调用与实参匹配的构造函数。
复制初始化调用复制构造函数。创建一个临时的对象,然后复制构造函数将那个临时的对象复制到正在创建的对象。
作为一般规则,初始化容器元素,分配一个空容器并将一直元素的值加入容器。
合成的复制构造函数:逐个将成员初始化。
class Sales_item
{
private:
std::string isbn;
int units_sold;
double revenue;
};
Sales_item::Sales_item(const Sales_item &orig):
isbn(orig.isbn),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
构造函数一般不应设置为explicit。复制构造函数应将实参的成员复制到正在构造的对象。
必须定义复制构造函数:
- 有些类必须对复制对象时发声的事情加以控制。类有数据成员是指针,或者有成员表示在构造函数中分配的其他资源。
- 另一些类在创建新对象时必须做一些特定工作。
禁止复制:例如iostream类。如果不定义复制构造函数,编译器将合成一个。
**为了防止复制,类必须显式声明其复制构造函数为private.**编译器将拒绝任何进行复制的尝试。
但类的友元和成员仍可以进行赋值。
如果连友元和成员的复制也禁止,声明一个private复制构造函数但不对其定义。
用户代码中复制尝试将在编译时标记为错误,成员函数和友元的复制尝试将在链接时导致错误。
2.赋值操作符
class Sales_item
{
public:
Sales_item& operator=(const Sales_item &);
};
合成赋值操作符:会执行逐个成员赋值。
Sales_item&
Sales_item::operator=(const Sales_item &rhs)
{
isbn=rhs.isbn;
units_sold=rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
3.析构函数
撤销类对象时会自动调用析构函数。
动态分配的对象只有在指向该对象的指针被删除时才撤销。
撤销一个容器,元素总是按逆序撤销。
显式析构函数
析构函数通常用于在构造函数或在对象声明周期内获取的资源。
三法则:如果类需要析构函数,那么也需要赋值操作符和复制构造函数。
析构函数可以执行任意操作,是类设计者希望在该类对象的使用完毕之后执行的。
合成析构函数按对象创建时间的逆序撤销每个非static成员。
合成析构函数并不删除指针成员所指向的对象。
4. 消息处理
Message类和Folder类分别表示电子邮件消息和消息所出现的目录。
Message类上有save和remove操作,用于指定Folder中保存或删除信息。
使每个Message保存一个指针集,set中的指针指向该Message所在的Folder。每个Folder也保存着一些指针,指向它所包含的Message。
class Message
{
public:
Message(const std::string &str = "") : contents(str) {}
Message(const Message &);
Message &operator=(const Message &);
~Message();
void save(Folder &);
void remove(Folder &);
private:
std::string contents;
std::set<Folder *> folders;
//将自身Message的一个副本添加到指定Message的各Folder中,
//完成后,形参指向的每个Folder也将指向这个Message。
void put_Msg_in_Folders(const std::set<Folder *> &);
void remove_Msg_from_Folders();
};
//赋值Message时,必须将新建的Message添加到保存原Message的每个Folder中。
//这个工作超出了合成后遭函数的能力范围。
/*
除了初始化以外,还必须用folders进行迭代,
将这个新的Message加到那个集合的每个Folder中。
*/
Message::Message(const Message &m) : consts(m.contents), folders(m.folders)
{
put_Msg_in_Folders(folders);
}
void Message::put_Msg_in_Folders(const set<Folder *> &rhs)
{
for (std::set<Folder *>::const_iterator beg = rhs.begin();
beg != rhs.end(); ++beg)
{
//接触迭代器引用,获得一个指向Folder的指针。
//将this传给addMsg,该指针向我们想要添加到Folder中的Message
(*beg)->addMsg(this);
}
}
//首先检查左右操作数是否相同。
//如果是不同对象,调用删除函数从每个Folder中删除该Message。将contents和folder成员赋值给这个对象。
//调用put 指向这个Message的指针添加到指向rhs的每个Folder中。
Message &Message::operator=(const Message &rhs)
{
if (&rhs != this)
{
remove_Msg_from_Folders();
contents = rhs.contents;
folders = rhs.folders;
put_Msg_in_Folders(ths.folders);
}
return *this;
}
void Message::remove_Msg_from_Folders()
{
for (std::set<Folder *>::const_iterator beg =
folders.begin();
beg != folder.end(); ++beg)
{
(*beg)->remMsg(this);
}
}
Message::~Message()
{
remove_Msg_from_Folders();
}
编写自己的复制构造函数时,必须显式复制需要赋值的任意成员。
显式定义的复制构造函数不会进行任何自动复制。
4.指针成员
包含指针的类需要特别注意复制控制,复制指针时只复制指针中的地址,而不会复制指针指向的对象。
管理指针:
- 指针成员采取常规指针型行为。这类指针具有所有缺陷但无需特殊的复制控制。
- 类可以实现所谓“只能指针”行为。指针指向的对象是共享的,但类能够防止悬垂指针。
- 类采取 值型 行为。指针所指向的对象是唯一的,由每个类对象独立管理。
class HasPtr
{
public:
//接受两个形参,将他们复制到HasPtr的数据成员。
HasPtr(int *p, int i) : ptr(p), val(i) {}
int *get_ptr() const { return ptr; }
int get_int() const { return val; }
void set_ptr(int *p) { ptr = p; }
void get_int(int i) { val = i; }
int get_ptr_val() const { return *ptr; }
void set_ptr_val(int val) const { *ptr = val; }
private:
int *ptr;
int val;
};
int obj = 0;
HasPtr ptr1(&obj,42);// 指向obj val = 42
HasPtr ptr2(ptr1);//指向obj val = 42
智能指针
新的HasPtr类需要一个析构函数来删除指针。删除指针的时候需要引入一个使用计数。
智能指针类将一个计数器与类指向的对象向关联。
每次创建类的新对象时,初始化指针并将使用计数置为1.当对象作为一个对象的副本而创建的时候,复制构造函数复制指针并增加与之相应使用的使用计数的值。
使用计数类
//U_Ptr类保存指针和使用计数
class U_Ptr
{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p) : ip(p), use(1) {}
~U_Ptr() { delete ip; }
};
class HasPtr
{
public:
HasPtr(int *p, int i) : ptr(new U_Ptr(p)), val(i) {}
HasPtr(const HasPtr &orig) : ptr(orig.ptr), val(orig.val) { ++ptr->use; }
HasPtr &operator=(const HasPtr &);
~HasPtr()
{
if (--ptr->use == 0)
delete ptr;
}
private:
U_Ptr *ptr;
int val;
};
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if (--ptr->use == 0)
{
delete ptr;
}
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
class HasPtr
{
public:
int *get_ptr() const { return ptr->ip; }
int get_int() const { return val; }
void set_ptr(int *p) { ptr->ip = p; }
void get_int(int i) { val = i; }
int get_ptr_val() const { return *ptr->ip; }
void set_ptr_val(int val) const { *ptr->ip = val; }
private:
U_Ptr *ptr;
int val;
};
定义值型类
给指针成员提供值语义。其行为很像算数类型的对象:复制值型对象时,会得到一个不同的新副本。对副本所做的改变不会反应在原有对象上。
要使指针成员表现的像一个值,复制HasPtr对象时必须复制指针所指向的对象。
//U_Ptr类保存指针和使用计数
class U_Ptr
{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p) : ip(p), use(1) {}
~U_Ptr() { delete ip; }
};
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if (--ptr->use == 0)
{
delete ptr;
}
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
class HasPtr
{
public:
HasPtr(const int &p, int i) : ptr(new int(p)), val(i) {}
HasPtr(const HasPtr &orig) : ptr(new int(*orig.ptr)), val(orig.val) {}
HasPtr &operator=(const HasPtr &);
~HasPtr() { delete ptr; }
int get_int() const { return val; }
void set_ptr(int *p) { ptr = p; }
void get_int(int i) { val = i; }
int get_ptr_val() const { return *ptr; }
void set_ptr_val(int val) const { *ptr = val; }
private:
int *ptr;
int val;
};
复制构造函数不再复制指针,将分配一个新的int对象,并初始化对象已保存与被复制对象相同的值。每个对象保存自己的副本,析构函数将无条件删除指针。
赋值操作符不需要分配新对象,只是必须给其指针所指向的对象赋新值,而不是给指针本身赋值。
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
*ptr = *rhs.ptr;
val = rhs.val;
return *this;
}
第十二天
1.重载操作符
重载操作符必须具有一个类类型操作数。
作为类成员的重载函数,有一个隐含的this形参,限定为第一个操作数。
//隐含this指针为第一个形参
Sales_item& Sales_item::operator+=(const Sales_item&);
Sales_item operator+(const Sales_item&.4,const Sales_item&);
操作符定义为非成员函数时,通常必须将它们设置为操作类的友元。通常需要访问类的私有部分。
使用重载操作符
cout<<item1+item2<<endl;
cout<<operator+(item1,item2)<<endl;
重载逗号、取地址、逻辑与、逻辑或等操作 具有有用的内置含义重载这些操作不是好做法。
选择成员或非成员实现
- 赋值,下标[ ],调用( ),和成员访问箭头->等操作符必须定义为成员,将这些操作符定义为非长远函数将在编译时标记为错误。
- 复合赋值操作符通常定义为类的成员。
- 改变对象状态或给定类型紧密联系的其他一些操作符。通常就定义为类成员
- 对称的操作符,最好定义为普通非成员函数
输出操作符<<重载
为了与IO标准库一致,操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参。并返回ostream的引用。
ostream& operator<<(ostream& os,const ClassType &object)
{
os<< ...
return os;
}
ostream& operator<<(ostream& out,const Sales_item& s)
{
out<<s.isbn<<s.units_sold<<s.revenue<<s.avg_price();
return out;
}
对于输出操作符,做最小限度的格式化。
io操作符必须为非成员函数。否则,左操作数只能是该类类型的对象。
如果想要支持正常用法,左操作数必须为ostream类型。如果该操作符是类的成员,则必须是ostream类的成员,然而ostream类是标准库的组成成分。
输入操作符<<
第二个形参必须是非const的,以为输入的目的就是将数据读取到这个对象中。
并且,输入操作符必须处理错误和文件结束的可能性。
istream& operator>>(istream& in,Sales_item& s)
{
double price;
in>>s.isbn>>s.units_sold>>price;
if(in)
s.revenue = s.units_sold * price;
else
s-Sales_item();
return in;
}
处理输入错误,要确定错误恢复措施。
算术操作符和关系操作符
一般而言,将算术操作符定义为非常远函数。
Sales_item operator+(const Sales_item& lhs,const Sales_item& rhs)
{
Sales_item ret(lhs);
ret+=rhs;
return ret;
}
相等操作符
inline bool operator==(const Sales_item &lhs,const Sales_item &rhs)
{
return lhs.units_sold == ths.units_sild && lhs.revenue == rhs.revenue&& lhs.same_isbn(rhs);
}
inline bool operator!=(const Sales_item &lhs,const Sales_item &rhs)
{
return !(lhs==rhs);
}
下标操作符
下标操作符必须定义为类成员函数。
类定义下标操作符时,一般需要定义两个版本
- 一个为非const成员并返回引用
- 另一个为const成员并返回const引用
class Foo
{
public:
int &operator[](const size_t);
const int &operator[](const size_t) const;
private:
vector<int> data;
};
int& Foo::operator[](const size_t index)
{
return data[index];
}
const int& Foo::operator[](const size_t index) const
{
return data[index];
}
成员访问操作符
箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但是把它作为成员一般也是正确的。
枯燥。。。看不下去了
第十三天
1.面向对象编程OOP
数据抽象、继承和动态绑定:
用类进行抽象,用类派生 从一个类继承另一个。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
动态绑定:编写程序时,使用继承层次中任意类型的对象。
void print_total(ostream &os,const Item_base &item,size_t n)
{
os<<item.book()<<n<<item.net price(n)<<endl;
}
基类
class Item_base
{
public :
Item_base(const std::string &book="",double sales_price = 0.0):
isbn(book),price(sales_price){}
std::string bool() const{return isbn;}
virtual double net_price(std::size_t n) const{return n* price;}
virtual ~Item_base(){}
private:
std::string isbn;
protected:
double price;
};
继承层次的根类一般都要定义虚析构函数。
virtual的目的是启动动态绑定。
除了构造函数以外,任意非static成员函数都可以是虚函数。
protected:类外部不能访问,派生类可以访问。
- 派生类只能通过派生类对象访问其基类的protected成员。派生类对其基类类型对象的protected成员没有特殊访问权限。
定义派生类
class Bulk_item :public Item_base
{
public:
double net_price(std::size_t) const;
private:
std::size_t min_qty;
double discount;
};
派生类中的虚函数可以返回基类函数所返回类型的派生类的引用或指针。
例如,Item_base类可以定义返回Item_base的虚函数,Bulk_item类中定义的实例可以定义为返回Item_base或Bulk_item*。
2.virtual
- 只有指定的虚函数的成员函数才能进行动态绑定,非虚构函数不进行动态绑定。
- 必须通过基类类型的引用或指针进行函数调用。
因为可以使用基类类型的指针或引用来引用派生对象,所以,使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:可基类类型,也可派生类类型。
编译器都把它当做基类类型对象。
在运行时确定virtual函数的调用
如果调用非虚类函数,则执行基类类型所定义的函数(编译时)。如果调用虚函数,则直到运行时才能确定调用哪个函数。
只有通过引用或指针调用,虚函数才在运行时确定。
覆盖虚函数机制:作用域操作符
Item_base *baseP = &derived;
double d = baseP->Item_base::net_price(42);
强制将net_price调用确定为Item_base中定义的版本。在编译时确定。
只有成员函数中的代码才应该使用作用于操作符覆盖虚函数机制。
默认实参
- 如果省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。
- 通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数中声明中指定的值。
- 如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本汇总声明的值。
3.继承
- 如果是公用继承,public – public protect – protect
- 如果是受保护继承,基类的public和protected成员在派生类中时protected成员。
- 如果是私有继承,基类的所有成员在派生类中时private成员。
派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中指定的更宽松或更严格。
class Base
{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
};
class Derived : private Base
{
public:
using Base::size;
protected:
using Base::n;
};
using 可以使size成员能够被用户访问,并使n能够被从Derived派生的类访问。
使用class保留字定义的派生默认具有private继承。一般使用显式private更清晰。
struct保留字定义的类默认具有public继承。
友元关系不能继承
如果基类定义static成员,则整个继承层次中只有这一个这样的成员。
用派生类对象对基类对象进行初始化或赋值时:
- 基类可能显式定义了将派生列对象赋值或复制给基类对象的含义。可以通过定义适当的构造函数或赋值操作符实现。(不太可能)
- 基类一般定义自己的复制构造函数和复制操作符,这些成员接受一个形参,该形参是基类类型的引用。
Item_base item;
Bulk_item bulk;
Item_base item(bulk)l
item = bulk;
每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
向基类构造函数传递实参
class Bulk_item :public Item_base
{
public :
Bulk(const std::string& book,double sales_price,
std::size_t qty = 0,double disc_rate =0.0):
Item_base(bool,sales_price),
min_qty(qty),discount(disc_rate){}
};
初始化基类和成员提供初始值,首先初始化基类,然后按照声明次序初始化派生类的成员。
只能初始化直接基类,然后通过直接基类对象初始化间接基类。
4 重构
重构包括:重新定义类层次,将数据从一个类移动到另一个类。改变了继承层次。
class Disc_item:public Item_base
{
public :
Disc_item(const std::string& book="",
double sales_price = 0.0,
std::size_t qty = 0,double disc_rate = 0.0):
item_base(book,sales_price),
quantity(qty),discount(disc_rate){}
protected:
std:size_t quantity;
double discount;
};
尊重基类接口:一个类一旦定义了自己的接口,与该类对象化的所有交互都应该通过该接口。
派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。
只包含类类型或内置类型数据成员,不含指针的类一般可以使用合成操作。
具有与指针成员的类一般需要定义自己的复制来控制管理这些成员。
定义派生类复制构造函数
如果派生类显式定义自己的复制构造函数或赋值操作符,那么该定义将完全覆盖默认定义。
被继承类的复制构造函数和赋值构造操作符负责对基类成分以及类自己的成员进行赋值或复制。
复制构造函数:
class Base{};
class Derived:public Base
{
public :
Derived(const Derived& d):
Base(d);
};
Base(d); 将派生类对象d转换为它的基类部分的引用。并调用基类复制构造函数。
赋值操作符:必须防止给自身赋值
Derived &Derived::operatore=(const Derived &rhs)
{
if(this!=rhs)
{
Base::operator=(rhs);
}
return this;
}
析构函数:
派生类析构函数不负责撤销对基类对象的成员。每个析构函数值负责清楚自己的成员。
classs Derived: public Base
{
public:
~Derived(){}
};
虚析构函数
自动调用基类部分的胸骨函数对基类的设计有重要影响。
基类中的析构函数必须为虚函数。通过指针调用时,会因为指针所指对象类型的不同而不同。
class Base_item
{
public :
virtual ~Item_bae(){}
};
Item_base *itemP = new Item_base;
delete itemP;
itemp = new Bulk_item;
delete itemp;
构造函数不能定义为虚函数,因为构造函数时在对象完全构造之前运行的,对象的动态类型还不完整。
赋值操作符构造为虚函数没有什么用处。
名字冲突(避免冲突)
在基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
可以使用作用于操作符访问被屏蔽的基类成员。
找到名字相同 就算参数不同也停止查找了。
struct Base{
int memfcn();
};
struct Derived:Base
{
int memfcn(int);
};
重载函数
如果派生类想通过自身类型使用的重载版本,则派生列要么重定义所有重载版本。要么一个也不重定义。
函数调用:
- 首先确定进行函数调用的对象,引用或指针的静态类型。
- 在该类中查找函数,如果找不到,就直接在基类中寻找。如果找不到,则调用就是错误的。
- 找到该名字,进行常规类型检查。
- 如果调用合法,编译器就生成代码。如果是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本。
纯虚函数 cosnt = 0;
继承了基类函数,但是这个基类函数在当前派生类中没有意义,然后又防止派生类对象的调用,设定纯虚函数。
class Dist_item:public Item_base
{
public :
double net_price(str::size_t) const = 0;
};
容器与继承
如果用一个容器去存放一些类对象,如果类型是基类,就会切掉派生类对象。
唯一可行的选择可能是使用容器保存对象的指针。但是需要用户面对管理对象和指针的问题。
句柄类和继承
C++不能使用对象支持面向对象变成,必须使用指针或引用。
句柄类存储和管理基类指针。指针所指对象的类型可以变化。用户通过句柄类访问继承的操作。
Sales_item item(Bulk_item("0",35,3,,20));
item->net_price();
定义句柄:默认构造函数、复制构造函数和接受Item_base对象的构造函数。
Sales_item类有两个数据成员,都是指针:一个指向Item_base对象,另一个指向使用计数。
还定义解引用操作符和箭头操作符。
class Sales_item
{
public:
Sales_item():p(0),use(new strd::size_t(1)){}
Sales_item(const Item_base&);
Sales_item(const Sales_item &i):
p(i,p),use(i.use){++*use;}
~Sales_item(){decrt_use();}
const Item_base *operator->() const {if(p) return p;}
const Item_base &operator* const(if(p) return *p;}
private:
Item_base *p;
std::size_t *use;
void decr_use(){if(--*use==0{deletep;delete use;}}
};
//赋值操作符
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
++*rhs.use;
decur_use();
p = rhs.p;
use = rhs.use;
return *this;
}
复制未知类型
class Item_base
{
public:
virtual Item_base* clone() const
{
return new Item_base(*this);
}
};
class Bulk_base:Item_base
{
public:
Bulk_base* clone() const
{
return new Bulk_base(*item);
}
}
//构造函数
Sales_item::Sales_item(const Item_base &item):
p(item.clone()),use(new std::size_t(1)){}
容器的比较器
定义一个类型别名:将comp定义为函数类型指针的同义词。
typedef bool (*Comp)(const Sales_item&,const Sales_item&);
std::multiset<Sales_item,Comp> items(compare);
items是一个multiset,保存Sales_item对象并使用Comp类型的对象比较他们。
class Basket
{
typedef bool(*Comp)(const Sales_item&,const Sales_item&);
public:
typedef std::multiset<Sales_item,Comp> set_type;
typedef set_type::size_type size_type;
typedef set_type::const_iterator const_iter;
Basket():items(compare){} //初始化一个比较器
void add_item(const Sales_item &item)
{
item.insert(item);
}
size_type size(const Sales_item &i) const
{
return items.count(i);
}
double total() const;
private:
std::multiset<Sales_item,Comp> items;
};
double Basket::total() const
{
double sum = 0.0;
for(const_iter iter = items.begin();iter!=items.end();iter = items.upper_cound(*iter))
{
sum+=(*iter)->net_price(items.count(*iter)):
}
return sum;
}
文本查询示例