类名::
构造函数没有返回值,如果自己定义了构造函数,默认构造函数需要显示定义类名()=default
构造函数语法类名(int a,int b):成员变量名(a),成员变量名(b){}
,某个成员变量被构造函数初始值列表忽略时,他将以合成默认构造函数相同的方式隐式初始化。
可以在类的外部定义构造函数,需要显示说明属于哪个类
构造函数一开始执行,初始化就完成了,如果成员是const,引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员函数提供初始值,不能在构造函数的构造体内为这些成员变量赋值!!
成员变量初始化的顺序和定义的顺序一致,和构造函数初始值列表中这些值的顺序无关。
尽量避免使用某些成员初始化其他成员
委托构造函数,委托的构造函数的初始值列表仅有一个那就是被委托的构造函数,然后加括号,里面填写被委托的构造函数初始值列表中的值。
class myclass{
public:
myclass(int a,int b,int c): aa(a),bb(b),cc(c){}
//其余构造函数采用上面的构造函数进行委托
myclass(int a):myclass(a,0,0){}
myclass(int b):myclass(0,b,0){}
}
如果想定义一个使用默认构造函数的类类型,不要加括号Myclass obj;
,如果加括号则声明了一个函数。
编译器会进行隐式类类型转换,此时将会调用相应的构造函数,定义一个类的临时对象。编译器只会转换一次。我们可以在构造函数前面声明为explicit,则编译器无法通过该构造函数隐式的创建类的临时实例对象。对于explicit的构造函数,我们只能使用直接初始化,而不能使用拷贝初始化。但我们可以显式的强制转换explicit构造函数fun(Myclass(null_book))
,此时myclass的构造函数时explicit类型,fun的输入时myclass类类型,此时构造函数创建了一个临时的myclass类类型传递给fun函数。
constexpr构造函数,函数体一般是空的,该构造函数必须初始化所有的数据成员。
默认情况,this指向类类型非常量版本的常量指针,因此我们不能把this绑定在一个常量对象上,这就使我们无法对一个常量类型引用普通成员函数。通过把成员函数设置成常量成员函数可以解决这个问题int fun() const {}
return *this;返回这个实例化对象。
定义在类里面的成员函数是隐式内联的,我们可以在定义在类外面的成员函数前面加上inline来显示指示这是内联函数。(一般不要在类内成员函数声明时使用inline)
我们把成员变量声明为mutable,无论实例对象是否是const都可以改变该变量mutable int a
如果我们在常量成员函数中返回*this,则返回的是常量this,但我们调用该成员函数的实例对象可能是非常量。于函数重载类似,我们需要为该函数定义两个版本(const和非const版本)。如果实例对象是非常量会调用非常量版本,此时实际执行的函数还是常量函数,但是返回this指针的函数是非常量函数,可以返回非常量指针,
class screen{
public:
screen &display(std::ostream &os){ do_display(os); return *this;}
const screen &display(std::ostream &os) const {do_display(os); return *this;}
private:
void do_display(std:ostream &os) const {os<<contents;}
}
类中可以包含指向它自身类型的指针或者引用。
我们可以在类外部使用类内部定义的类型,需要加上定义域说明符
当成员函数定义在类的外部时,返回类型中使用的名字都应该位于类的作用域之外,此时必须明确指明返回类型是哪个类的成员window_mgr::ScreenIndex window_mgr::fun(){}
编译器处理所有有的声明后才会处理成员函数的定义。
静态成员,通过在成员的声明前加上关键字static使得它和类关联在一起。我们使用作用域运算符leimign::静态成员名
,或者对象访问静态成员,成员函数可以不通古作用域运算符直接使用静态成员。
定义静态成员:在类的外部定义静态成员函数,不能重复使用static关键字。==我们不能再类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员函数和变量。==在类的外部定义静态成员的方式和在类的外部定义成员函数的语法类似。
我们可以为静态数据成员提供const int类型的类内初始值,不过该静态成员必须是字面值常量类型的constexpr,static constexpr int period=30
如果在类的内部为静态数据成员提供了一个初始值,则成员的类外定义不能再指定一个初始值了
我们可以使用静态成员作为类内成员函数的默认实参,而无法使用普通成员变量作为类内成员函数的默认实参。
friend int fun();
friend void window_mgr::clear(ScreenIndes);
。此时我们需要先定义windows_mgr类,声明其中的clear函数,但是不能定义它,在使用screen中的screenindex时,还要先声明screen。接下来我们定义screen,包含对clear的友元声明,最后定义clear,此时它才可以使用screen的成员。–>需要定义为其他类友元的函数建议在类外定义而不是类内定义。friend int fun(){}
,我们也必须在类外声明该函数才可以使用该友元函数。拷贝构造函数:
class Sales_data{
public:
Sales_data(const Sales_data &);
private:
string bookNo;
int units_sold=0;
double revenue=0.0;
}
//拷贝构造函数
Sales_data::Sales_data(const Sales_data &ori):
bookNo(ori.bookNo),units_sold(ori.units_sold),revenue(ori.revenue){}
拷贝赋值运算符:
class Foo{
public:
Foo& operator=(const F00&)
}
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo=rhs.bookNo;
//...
return *this;//返回此对象的一个引用
}
析构函数
class Foo
{
public:
~Foo();
}
阻止拷贝:
NoCopy()=default;
NoCopy(const NoCopy&)=delete;
NoCopy &operator=(const NoCopy&)=delete;
~NoCopy()=default;
移动构造函数和移动赋值运算符:
StrVec::StrVec(StrVec &&s) noexcept//移动操作不应该抛出任何异常
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//令s进入这样一种状态,对其运行析构函数是安全的
s.elements=s.first_free=s.cap=nullptr;
}
StrVec &StrVec::operator=(StrVec &&rhs) noexpect
{
if(this!=&rhs){
free();
elements=rhs.elements;
first_free=rhs.first_free;
cap=rhs.cap;
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
三五法则:
对象行为:
对于管理资源的类,我们根据实际需要,把该类的行为定义为值行为或者指针行为。如果是指针行为最好用智能指针,也可以自己实现引用计数和普通指针。
对于值行为的类,定义拷贝赋值运算符时,应该先拷贝值,再释放内存。否在无法规避自赋值的问题。(先释放再拷贝会导致自赋值拷贝时访问一个无效内存的指针)
引用计数:自己实现的引用计数必须保存在共享内存中,普通构造函数创建的引用技术为1。拷贝构造函数的函数体中把引用计数+1,析构函数需要先判断引用计数–是否为0,如果为0释放指针指向的内存。拷贝赋值运算符先增加右侧的引用计数再递减左侧的引用计数(保持自赋值的时候引用计数不变),如果左侧引用计数为0,释放左侧对象指针指向的空间。最后把右侧对象拷贝给左侧对象。
交换操作:
class HasPtr{
friend void swap(HasPtr&,HasPtr&);
};
inline void swap(HasPtr &l,HasPtr &r){
using std::swap;
swap(l.ps,r.ps);
swap(l.i,r.i);
}
HasPtr& HasPtr::operator=(HasPtr rhs){
swap(*this,rhs);
return *this;
}
消息和文件:
通常来说,分配资源的类更需要拷贝控制,但资源管理并不是一个类需要拷贝控制的唯一原因。下面我们将给出类需要拷贝控制来进行簿记操作的例子。
一个消息可以放在多个floder中,一个floder中存放多条消息。Message类提供save和remove操作,来向一个floader添加消息或者删除消息。创建的新的message不会指出floder。拷贝消息时候,副本和元对象是不同的message对象,但两个message都出现在相同的floder中。
class Message{
friend class Floder;
public:
explicit Message(const string &str=""):contents(str){}
Message(const Message&);
Message& operator=(const Message&);
~Message();
void save(Floder&);
void remove(Floder&);
private:
string contents;
set<Floder*> floders;
//将本Message添加到Floader中
void add_to_Floders(const Message&);
//从floder中删除message
void remove_from_Floders();
}
void Message::save(FLoder &f){
floders.insert(&f);
f.addMsg(this);
}
void Message::remove(Floder &f){
floder.erase(&f);
f.remMsg(this);
}
void Message::add_to_Floders(const Message &m){
for (auto f:m.floders)
f->addMsg(this);
}
Message::Message(const Message &m):contents(m.contents),floder(m.floders)
{
add_to_Floders(m);
}
void Message::remove_from_Folders()
{
for(auto f:floders)
f->remMsg(this);
}
Message::~Message(){
remove_from_Floders();
}
Message& Message::operator=(const Message &rhs){
//先删除指针再插入他们来处理自赋值情况
remove_from_Floders();
contents=rhs.contents;
floders=rhs.floders;
add_to_Floders(rhs);
return *this;
}
//swap前后,消息和文件列表的逻辑关系没变,但是消息的地址变化了,所以文件中保存的消息的地址也要变化。
void swap(Message &lhs,Message &rhs){
using std::swap;
from (auto f:lhs.floders)
f->remMsg(&lhs)
from (auto f:rhs.floders)
f->remMsg(&rhs)
swap(lhs.floders,rhs.floders);
swap(lhs.contents,rhs.contents);
//从新把message插入到floder中
from (auto f:lsh.floders)
f->addMsg(&lhs);
from (auto f:rsh.floders)
f->addMsg(&rhs);
}
自定义Vector
在自定义的StrVec中,我们用alloctor来获得原始的动态内存,在添加新元素的时候,用aclloctor的construct成员创建对象。为了维护这一个序列,我们需要三个指针,elements指向分配内存的首地址,first_free指向最后一个实机未分配的地址,cap指向分配的内存末尾之后的地址。
class StrVec{
public:
StrVec():
}
void StrVec::push_back(const string& s)
{
chk_n_alloc();
alloc.construct(first_free++,s);
}
void StrVec::push_back(string &&s){
chk_n_alloc();
alloc.construct(first_free++,std::move(s));
}
class Foo{
Foo &operator=(const Foo&) &;// 最后一个&是引用限定符号。只能向左值赋值
}
调用重载运算符data1+2;operator+(data1,data2);data1.operator+=(data2);
重载运算符会保留运算符的结合律,但是无法保留求职顺序和短路求值属性。不建议重载逻辑与,或,逗号,和取址运算符。也不建议重载与或运算符。
具有对称性的运算符可能转换为任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此他们通常应该定义为普通的非成员函数,其他情况通常定义为成员函数。
**原因:**我们可能希望在混合类型中调用算术运算符,比如int和double的和,因此就不能把该运算符重载为int或者double对象的成员函数。
**重载输出与运算符:**输出运算符的返回值是ostream的引用类型,第一个参数是ostream的引用,因为我们无法复制一个ostream对象,并且输出一般会改变流的状态因此不能是const。第二个参数一般是要输出对象的常量引用,输出我们一般不改变被输出的对象因此是常量,为了避免复制,我们采用引用形式。
Ostream & operator<<(ostream &os,const Sales_data &item)
{
os<<item.isbn()<<" "<<item.units_sold
reutrn os;
}
**重载输入运算符:**同输出类似,不同之处在于第二个形参不能是const,因为要写入内容。
istream & operator>>(istream &is, Sales_data &item)
{
double price;
is>>item.bookNo>>item.units_sold>>price;
if (is)
item.revenue=item.units_sold*price;
else
item=Sales_data();//写入失败,对象被赋予默认的状态
reutrn is;
}
Sale_data operator+(const Sales_data &r,const Sales_data &l){
Sales_data sum=r;
sum.item+=l.item;
return sum;
}
bool operator==(const data &a,const data&b){
return a.item1==b.item1 && a.item2==b.item2;
}
//基于等于定义不等于
bool operator!=(const data &a,const data&b){
return !(a==b);
}
StrVec &StrVec::operator=(initializer_list<string> il)
{
auto data=alloc_n_copy(il.begin(),il.end());
free();
elements=data.first;
first_free=cap=data.second;
return *this;
}
data &data::operator+=(const data &item);
{
unit+=item.unit;
revenut+=item.revenut;
return *this;
}
为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。进一步,我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。
std::string & operator[](std::size_t n)
{return elements[n]}
const std::string & operator(std::size_t n) const
{return elements[n]}
p.operator++(0)
class StrBlobPtr{
public:
StrBlobPtr& operator++();//前置运算符
StrBlobPtr& operator++(int);//后置运算符
}
StrBlobPtr& strBlobPtr::operator++()
{
check(curr,"increament pass end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr& strBlobPtr::operator++(int)
{
StrBlobPtr ret=*this;
++*this;//解引用并且递增,解引用得到的是StrBlobPtr类型的对象,递增调用的是该对象重载的前置递增运算符。
return ret;
}
class StrBlobPtr{
public:
std::string& operator*() const{
auto p=check(curr,"diasdf");
return (*p)[curr];//*p返回的是指向某个对象的指针,该对象是一个vector或者列表,可以用下标运算符。
}
std::string* operator->() const{
return & this->operator*();
}
}
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象(python中的call魔法方法)。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。
类重写了函数调用运算符我们叫做函数对象,函数对象常常作为泛型算法的实参==(函数对象可以相当于函数使用,任何需要函数的地方都可以传递函数对象)==。
labmda是函数对象。编译器会将lambda翻译成未命名的函数对象。
lambda通过引用捕获变量时,将由程序负责确保引用对象的存在,编译器创建的未命名函数对象不会存储捕获的成员。相反值捕获变量会被拷贝到lambda中。
标准库定义的函数对象,这些类都被定义成了模板的形式。
表示运算符的函数对象类常用来替换算法中的默认运算符。在默认情况下排序算法使用operator<将序列按照升序排列。如果要执行降序排列的话,我们可以传入一个greater类型的对象。
sort(svec.begin(),svec.end(),greater
此外,如果我们想要使用指针的地址大小来排序,我们只能用标准库提供的less
**类型转换运算符:**类型转换定义了类类型如何转换为其他类型,type()为目标类型operator type() const;
我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型
我们可以定义显示的类型转换explicit operator type() const:
,在if ,while,do,for以及其他逻辑判断中,编译器会隐式执行显示类型转换。
无论我们什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换。如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假。
一些转换trick:
对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。
不要对两个类定义相互的类型转换
不要定义两个及以上的相同转换源。例如在B类中定义类构造函数从A中转换,又定义了转换为A的类型转换运算符。这种情况下就会产生二义性。此时我们必须显示的调用类型转换运算符或者转换构造函数。
另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题。
当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,则我们认为这些类型转换一样好。在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别。下面会出现二义性
struct A{
A(int);
}
struct B{
B(double);
}
void manip(const &A);//1
void manip(const &B);//2
manip(10);//二义性!!!可以调用1也可以调用2
**虚函数:**基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。派生类中函数最后添加关键字override。派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
**运行时绑定(动态绑定):**在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
class NoDSerived final {}
class Quote{
public:
Quote()=default;
Quote(const std::string &book. double sales_price):bookNo(book),price(sales_price){}
std::string isbn() const {return bookNo;}
virtual double net_price(std::size_t n) const{ return n*price;}
virtual ~Quote()=default;
private:
std::string bookNo;
protected:
double price=0.0;
}
class Bulk_quote::public Quote{
public:
Bulk_quote()=default;
Bulk_quote(const std::string&,double,std::size_t,double);
Bulk_quote(const std::string &book,double p,std::size_t qty,double disc):Quote(book,p),min_qty(qty),discount(disc){}//调用基类的构造函数
double net_price(std::size_t)const override;
private:
std::size_t min_qty=0;
double discount=0.0;
}
普通类型调用虚函数,编译时就会将调用版本确定下来。
引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
在派生类中编写和基类虚函数有不同形参列表或者返回值的函数是符合语法的,编译器会认为这是不同的函数,但这么做可能和实际冲突。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错
注意:如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。
使用作用域运算符可以强制使用虚函数的某个特定版本double un=baseP->Quote::net_price(43)
。改代码片段无论baseP实际指向基类还是派生类,都执行基类(Quote)的net_price()函数。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。
**纯虚函数:**和普通的虚函数不一样,一个纯虚函数无须定义(没有实际意义)。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处:值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。
double net_price(std::size_t) const = 0;
void clobber(S &s){s.j=s.promt=0;}//clobber是S类型的成员函数,可以访问S对象基类的受保护成员
void clobber(B &b){b.promt=0;}//错误, S的成员无法访问基类对象的受保护成员。
class S: private B{//继承自B的成员是private
}
派生类(B是A的派生类)
中友类的成员(可以访问B中A类的成员)。例如P是Base的友元,S继承Base,Base中的prompt成员是protected。则在P类中,可以访问S类的prompt成员。class Base{
public:
size_t size() const{return n;}
protected:
size_t n;
}
class Derived :private Base{
public:
using Base::size;//私有继承,所以继承来的size是私有的,使用using声明语句改变了这些成员的可访问性。Derived的用户将可以使用size成员,而Derived的派生类将能使用n。
}
存在继承关系时,派生类的作用域嵌套在基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
派生类中定义的名字会屏蔽外层作用域中的名字(基类中的名字)。我们可以使用作用域运算符来使用隐藏的成员。
调用p->mem()的过程
首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。
在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。
一旦找到了mem,就进行常规的类型检查(参见6.1节,第183页)以确认对于当前找到的mem,本次调用是否合法。
假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
----如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。
----反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用
有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。一种好的解决方案是为重载的成员提供一条using声明语句。==using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。==此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义
**“继承”构造函数:**派生类能够重用其直接基类定义的构造函数。尽管如我们所知,这些构造函数并非以常规的方式继承而来,但是为了方便,我们不妨姑且称其为“继承”的。
见本人csdn主页