#include<iostream>
#include<string>
using namespace std;
struct Sales_data
{
Sales_data() = default;
Sales_data(const string &s) :bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(istream &);
string isbn()const
{
return bookNo;
}
Sales_data &combine(const Sales_data &);
double avg_price()const;
//数据成员
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum.combine(rhs); //把rhs的数据成员加到sum当中
return sum;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
//输入的交易信息包括ISBN编号、售出总数、售出价格
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
Sales_data::Sales_data(istream &is)
{
read(is, *this);//read 函数的作用是从is中读取一条数据然后存入this对象中
}
double Sales_data::avg_price()const
{
if (units_sold)
{
return revenue / units_sold;
}
else
return 0;
}
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}
string isbn()const
{
return bookNo;
}
说明: 当我们调用成员函数时,实际上是在替某个对象调用它。如果 isbn()指向 Sales_data
成员(例如bookNo), 则它隐式地指向调用该函数的对象的成员。
在上面的调用中,当isbn
返回bookNo
时, 实际上它隐式地返回total.bookNo
.
● 成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象。 当我们调用一个成员函数时,用请求该函数的对象地址初始化this。 例如, 如果调用
total.isbn();
则编译器负责把total
传递给了 isbn()
函数的隐式形参this
,等价于:
string isbn(&total) const ;
其中,调用Sales_data的isbn
成员时 传入了 total
的地址。
● 任何对类成员的直接访问都被看作this
的隐式引用。
注意: 任何自定义名为this
的参数或变量的行为都是非法的。
注意: 因为this
的目的总是指向“这个”对象, 所以this
是一个常量指针, 我们不允许改变this
中保存的地址。
引入 const 成员函数
● isbn() 函数的参数列表之后的const
关键字。 const
的作用是修改隐式this
指针的类型(const Sales_data *const)
● 在默认情况下,this
的类型是指向类类型非常量版本的常量指针。
尽管this
是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下) 我们不能把this
绑定到一个常量对象上。
这一情况也就使的我们不能在一个常量对象上调用普通的成员函数。
● 在C++语言中允许把const关键字放在成员函数列表之后,此时,紧跟在参数列表后面的const
表示this 是一个指向常量的指针(const Sales_data *const)。
像这样使用const
的成员函数被称作常量成员函数。
string isbn(const Sales_data *const this)
{
return this->bookNo;
}
注意: 上面的代码是非法的,因为我们不能定义自己的this指针,此处的this是一个指向常量的指针,因为isbn 是一个常量成员
因为此处的this
指针是指向常量的指针, 所以常量成员函数不能改变调用它的对象的内容。
注意 : 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
类作用域和成员函数
● 编译器分两步处理类: 首先编译成员的声明, 然后才轮到成员函数体(如果有的话)。 因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
定义一个返回*this对象的函数
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
//把调用该函数的对象当成一个整体来访问
return *this; //返回调用该函数的对象
}
● 内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了与它保持一致, conbine
函数必须返回引用类型。 因为此时的左侧运算对象是一个Sales_data
的对象, 所以返回类型应该是Sales_data&
.
● 一般来说,执行输出任务的函数(比如:print())应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum.combine(rhs); //把rhs的数据成员加到sum当中
return sum;
}
● 默认情况下,拷贝类的对象其实拷贝的是此对象的数据成员。
构造函数
● 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
● 注意: 不同于其他成员函数, 构造函数不能被声明成const
的,当我们创建类的一个const
对象时,直到构造函数完成初始化过程,对象才能被真正取得其“常量”属性。因此,构造函数在const
对象的构造过程中可以向其写值。
合成的默认构造函数
● 此函数的特性之一有: 如果我们的类没有显式地定义构造函数, 那么编译器就会为我们隐式地定义一个合成默认构造函数。
● 这个合成的默认构造函数将按照如下规则初始化类的数据成员:
如果存在类内的初始值,用它来初始化成员。
否则,默认初始化该成员
某些类不能依赖于合成的默认构造函数
● 对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:
(1) 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。 一旦我们定义了一些其他构造函数,那么除非我们再定义一个默认的构造函数, 否则类将没有默认构造函数。
这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。
(2) 对于某些类来说,合成的默认构造函数可能执行错误的操作。
含有内置类型或复合类型成员(数组和指针)的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。
(3) 有的时候不能为某些类合成默认的构造函数。例如: 如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数, 那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。
Sales_data() = default;
● 在C++11新标准中,如果我们需要默认的行为, 那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。
其中,= default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果* = default*在类的内部,则默认构造函数是内联的; 如果它在类的外部,则该成员默认情况下不是内联的。
拷贝、赋值和析构
● 对象在几种情况下会被拷贝:如果我们初始化变量以及以值的方式传递或返回一个对象等。
当对象不再存在时执行销毁的操作, 比如一个局部对象会在创建它的块结束时被销毁,当vector对象(或者数组) 销毁时存储在其中的对象也会被销毁。
注意: 如果我们不主动定义这些操作,则编译器将替我们合成他们。一般来说, 编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。
某些类不能依赖于合成的版本
● 注意: 很多需要动态内存的类能(而且应该) 使用vector
对象或者string
对象管理必要的存储空间。 使用vector
或者string
的类能避免分配和释放内存带来的复杂性。
进一步讲,如果类包含vector
或者string
成员,则其拷贝、赋值和销毁的合成版本能够正常工作。 当我们对含有vector
成员的对象执行拷贝或者赋值操作符时,vector
类会设法拷贝或者赋值成员中的元素。
当这样的对象被销毁时,将销毁vector
对象, 也就是依次销毁vector
中的每一个元素,这一点与string
是非常类似的。
友元
● 友元声明只能出现在类定义的内部, 但是在类内出现的具体位置不限。 友元不是类的成员,也不受它所在区域访问控制级别的约束。
● 注意: 一般来说,最好在类定义开始或结束前的位置集中声明友元。
可变数据成员
● 一个可变数据成员永远不会是const
, 即使它是*const对象的成员。 因此一个const
成员函数可以改变一个可变成员的值。* 任何成员函数,包括const
函数在内都能改变他的值。
● 当我们提供一个类内初始值时,必须以符号 = ,或者花括号表示。
返回*this的成员函数
#include<iostream>
#include<string>
using namespace std;
class Screen
{
public:
typedef string::size_type pos;
Screen() = default; //因为Screen 有另一个构造函数,所以本函数是必须的
//cursor被其类内初始值初始化为0
Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht*wd, c) {}
char get()const //读取光标处的字符
{
return contents[cursor];
}
inline char get(pos r, pos c)const;
Screen &move(pos &r, pos c);
void some_member()const;
Screen &set(char c)
{
contents[cursor] = c; //返回当前光标所在位置的新值
return *this; //将 this 对象作为左值返回
}
Screen &set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; //设置给定的值
return *this;//将 this 对象作为左值返回
}
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(ostream &os)const
{
do_display(os);
return *this;
}
private:
void do_display(ostream &os)const
{
os << contents;
}
mutable size_t access_ctr; //即使在一个const对象内也能被修改
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
inline Screen &Screen::move(pos &r, pos c)
{
pos row = r*width; //计算行的位置
cursor = row + c; // 在行内将光标移动到指定的列
return *this; //以左值的形式返回对象
}
char Screen::get(pos r, pos c)const
{
pos row = r*width; //计算行的位置
return contents[row + c]; //返回给定列的字符
}
void Screen::some_member()const
{
++access_ctr;
}
Screen &set(char c)
{
contents[cursor] = c; //返回当前光标所在位置的新值
return *this; //将 this 对象作为左值返回
}
Screen &set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; //设置给定的值
return *this;//将 this 对象作为左值返回
}
● 我们的set() 成员函数的返回值是调用 set的对象的引用。 返回引用的函数是左值,意味着这些函数返回的是对象本身而非对象副本。
● 注意 : 一个const
成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
基于const的重载
● 通过区分成员函数是否是const的,我们可以对其进行重载。
其原因为: 因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const
成员函数。
另一方面,虽然可以在非常量对象上调用常量版本或非常量版本, 但显然此时非常量版本是一个更好的匹配。
class Screen
{
public:
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(ostream &os)const
{
do_display(os);
return *this;
}
private:
void do_display(ostream &os)const
{
os << contents;
}
}
● 当display
的非常量版本调用do_display
时,它的this
指针隐式地从指向非常量的指针转换成指向常量的指针.
● 当do_display
完成后, display
函数各自返回解引用this
所得的对象。 在非常量版本中,this
指向一个非常量对象, 因此display
返回一个普通的(非常量)引用; 而const
成员则返回一个常量引用。
● 当我们在某个对象上调用display
时,该对象是否是const
决定了应该调用display
的哪个版本。
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(count); //调用非常量版本
blank。display(cout); //调用常量版本
友元再探
● 可以把普通的非成员函数定义成友元,类还可以把其他类定义成友元, 也可以把其他类(之前定义过的)的成员函数定义成友元。 此外,友元函数能定义在类的内部, 这样的函数是隐式内联的。
令成员函数作为友元
class Window_mgr
{
public:
void clear();
};
class Screen
{
friend void Window_mgr::clear(); //Window_mgr::clear()必须在Screen之前被声明
};
void Window_mgr::clear()
{
}
说明: 当一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类(目的是防止重载函数的二义性),在Window_mgr
的内部,函数clear()
的声明中不能使用关键字friend
。
这种方案的控制更多,而且安全性更高,只适用于Screen
类确信Window_mgr
类中没有其他方法需要访问Screen
中的成员。
注意: 友元函数并不在授予友元关系的类中实现(除非它是友元函数的内联声明和定义,这种情况很罕见)。 友元函数位于另一个类中,或者它可以是不属于任何类的自由函数。
注意: 在需要时,通过在类的头文件中放置友元声明,向指定的类和函数授予友元关系。
函数(或另一个类)不能宣称自己是另一个类的友元。
注意: 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
友元声明和作用域
class x
{
public:
friend void f() {/*友元函数可以定义在类的内部*/} // 友元本身不一定真的声明在当前作用域中
x() { f(); } // 错误: f还没有被声明
void g();
void h();
};
void x::g() // 错误: f还没有被声明
{
return f();
}
void f(); //声明那个定义在x中的函数
void x::h() // 正确: 现在 f的声明在作用域中了
{
return f();
}
● 类和非成员函数的声明不是必须在它们的友元声明之前。
甚至就算在类的内部定义了该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。 换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的
类的作用域
● 在 类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。 对于类类型成员则使用作用域运算符访问。 不论哪种情况, 跟在运算符之后的名字都必须是对应类的成员。
class Screen
{
public:
typedef string::size_type pos;
Screen() = default; //因为Screen 有另一个构造函数,所以本函数是必须的
//cursor被其类内初始值初始化为0
Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht*wd, c)
{
cout << "带三个参数的构造函数被调用!" << endl;
}
char get()const //读取光标处的字符
{
return contents[cursor];
}
inline char get(pos r, pos c)const;
Screen &move(pos &r, pos c);
void some_member()const;
Screen &set(char c)
{
contents[cursor] = c; //返回当前光标所在位置的新值
return *this; //将 this 对象作为左值返回
}
Screen &set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; //设置给定的值
return *this;//将 this 对象作为左值返回
}
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(ostream &os)const
{
do_display(os);
return *this;
}
private:
void do_display(ostream &os)const
{
os << contents;
}
mutable size_t access_ctr; //即使在一个const对象内也能被修改
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
inline Screen &Screen::move(pos &r, pos c)
{
pos row = r*width; //计算行的位置
cursor = row + c; // 在行内将光标移动到指定的列
return *this; //以左值的形式返回对象
}
char Screen::get(pos r, pos c)const
{
pos row = r*width; //计算行的位置
return contents[row + c]; //返回给定列的字符
}
void Screen::some_member()const
{
++access_ctr;
}
int main()
{
Screen::pos ht = 24, wd = 80; //使用Screen定义的pos类型
Screen scr(ht, wd, 's ');
Screen *p = &scr;
char c = scr.get(); //访问scr对象的get成员
cout << "先输出scr调用get() 函数的值:" << c << endl;
c = p->get(); // 访问p所指对象的get成员
cout << "在输出指针p调用get() 函数的值:" << c << endl;
system("pause");
return 0;
}
作用域和定义在类外部的成员
● 为什么我们在类的外部定义成原函数时必须提供类名和函数名。 在类的外部,成员的名字被隐藏起来了。
一旦遇到了类名,定义的剩余部分就在类的作用域之内了, 这里的剩余部分包括参数列表和函数体。 结果就是,我们可以直接使用类的其他成员而无须再次授权了。
● 另一方面, 函数的返回类型通常出现在函数名之前。 因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。
名字查找与类的作用域
● 名字查找:寻找与所用名字最匹配的声明的过程,从所在块开始,只考虑在名字的使用之前出现的声明,由内向外。
定义在类内部的成员函数,解析其中的名字方式与上述查找规则有所区别:
类的定义分两步:
首先,编译成员的声明
直到类全部可见后才编译函数体
编译器处理完类中的全部声明后才会处理成员函数函数定义
通常非类作用域的符号查找是按顺序的,其后面的被涉及的符号必须在前面出现过.如:
void foo()
{
};
void main()
{
int a= 1, b = 2;
int c = a + b; //涉及的a和b在前面出现过了.
int d = foo(); //涉及的foo在前面出现过了.
c = d + goo(); //编译报错,因为涉及的goo没有在前面出现过(其在本条语句的后面第一次出现)
}
void goo()
{
}
//////////////////////////////////////////////////////////////////////////////////////////////
对于类作用域(即类成员)的符号,我们不必要符号顺序出现,如:
class CFoo
{
public:
void hoo()
{
int c = m_aData + m_bData; //可以编译通过,虽然m_aData, m_bData声明在本条语句的后面,但是因为是类作用域,在类中都可见,所以这里是可以的.
}
private:
int m_aData,m_bData;
};
那么编译器是怎么实现类作用域的符号查找的呢?
分为两步编译:
第一步: .首先值编译类的所有成员的声明(而不是定义),如void CFoo::hoo()就是一个成员函数的声明,而其的函数体就是起定义.
这样的话,在此步遍编译结束后,编译器会把所有的符号声明存储起来,供第二步使用.
第二步: 在编译成员的定义(如函数体),此时如果遇到一个变量如m_aData,就会首先从第一步中的存储查找。
● 按照这种两阶段的方式处理类可以简化类代码的组织方式。 因为成员函数体直到整个类可见后才会被处理, 所以它能使用类中定义的任何名字。 相反,如果函数的定义和成员的声明被同时处理,那么我们不得不在成员函数中使用那些早已出现的名字
类型名要特殊处理
● 在类中,如果成员使用了外层作用域中的某个名字, 而且该名字代表一种类型, 则类不能在之后重新定义该名字:
typedef double Money;
class Account
{
public:
Money balance () //使用外层作用域的Money
{
return bal;
}
private:
typedef double Money; // 错误:不能重新定义Money,但是在VS上编译是正确的,但行为确实错误的。
Money bal;
};
注意: 类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
● 注意: 通常情况下不建议为参数(或者说局部变量)和成员使用同样的名字
在文件中名字的出现处对其进行解析
int height;
class Screen
{
public:
typedef string::size_type pos;
void setHeight(pos);
pos height = 0; //隐藏了外层作用域中的height
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var)
{
height = verify(var); //height 类的成员
}
● 当成员定义在类的外部时,不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。
构造函数在探
● 就对象的数据成员而言, 初始化和赋值也有一定的区别。如果没有在构造函数的初始值列表中显式初始化成员,则该成员将在构造函数体之前执行默认初始化。
● 如果成员是const,引用或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值
建议: 使用构造函数初始值(初始化和赋值的区别:初始化直接初始化数据成员,而赋值是先初始化,在赋值,所以养成使用构造函数初始值的习惯).
成员初始化的顺序
● 成员初始化的顺序: 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体顺序,成员初始化的顺序与他们在类定义中出现的顺序一致,第一个成员先初始化,然后第二个,以此类推。
构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
class X
{
int i;
int j;
public:
X(int val) :j(val), i(j) {} //未定义的:i在j之前被初始化
};
● 实际上,i先被初始化,因此这个初始值的效果是试图使用未定义的值j初始化i
● 最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员。如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数
● 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程, 在委托构造函数内,成员初始值列表只有一个唯一的入口, 就是类名本身(就是同类的其他构造函数的类名)。 类名后面的参数列表必须与类中另外一个构造函数匹配。
struct Sales_data
{
//非委托构造函数使用对应的实参初始化成员
Sales_data(const string &s, unsigned cnt, double price) :bookNo(s), units_sold(cnt), revenue(price*cnt) {}
//其余构造函数全都委托给另外一个构造函数
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(const string &s) : Sales_data(s, 0, 0) {}
Sales_data(istream &is) : Sales_data()
{
read(is, *this);
}
//数据成员
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
说明: 上述代码中, 接受istream &
的构造函数也是委托构造函数,它委托给了默认构造函数, 默认构造函数又接着委托给了三参数构造函数。
当这些受委托的构造函数执行完后, 接着执行istream&
构造函数体的内容。
注意: 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。 在Sales_data
类中, 受委托的构造函数体恰好是空的。
假如函数体包含代码的话,将先执行这些代码, 然后控制权才会交还给委托者的函数体。
在贴一段完整的代码感受一下:
#include<iostream>
using namespace std;
class Date
{
public:
//非委托构造函数使用对应的实参初始化成员
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
//其余构造函数全都委托给另一个构造函数
Date() :Date(1990, 1, 1)
{}
Date(int year) :Date() {}
private:
int _year;
int _month;
int _day;
};
void FunTest()
{
Date d;
Date::Date(2016);
}
int main()
{
FunTest();
system("pause");
return 0;
}
注意: 不要递归委托
#include <iostream>
using namespace std;
class A
{
private:
int i = 5;
string str = "初始值";
public:
A(string ss) :A(555) //错误,这里委托给了int型参数的构造函数,形成递归委托,这样就会在两个函数中来会执行,形成死循环(死递归)的构造函数
{
str = ss;
}
A(int ii) :A("OK") //错误,这里委托给了string型参数的构造函数,形成递归委托
{
//不能写成AA(int ii):A(),i(ii)
//委托构造函数不能再利用初始化器初始化其他数据成员
i = ii;
}
void show() {
cout << "i=" << i << ",str=" << str << endl;
}
};
int main()
{
A a(10);
a.show();
}
注意 : 在使用委托构造函数时,不能再进行成员列表初始化,而只能在函数体内进行初始化其他成员变量。那么我们在委托其他构造函数构造对象时,一些成员变量以默认值构造了,在函数体内进行初始化就不叫初始化了,只能叫重新赋值了。
class A
{
private:
int a;
int b;
char c;
char d;
public:
A(int num0, int num1, char C) :a(num0), b(num1), c(C) {}
A(int num0, char C) :A(num0, 0, C) {}//b默认初始化为0
A(int num0) :A(num0, 'p') { b = 1; }//b重新赋值为1
void getMembers()
{
cout << a << " " << b << " " << c << " " << d << endl;
}
};
在委托第二个构造函数构造时,b被初始化为0,这里我们在函数体内重新赋值为1,那么b到底是0还是1呢?结果是1。函数体内的初始化要晚于成员列表初始化,即委托其他构造函数构造完后,在进行函数体内的赋值。
注意 : 一个构造函数想要委托另一个构造函数,被委托的构造函数应该包含最大数量的参数,初始化较多的成员变量。被委托的构造函数并不用包含所有成员变量(没有包含的成员变量,以默认值进行初始化),然后被其他构造函数委托。
默认构造函数的作用
● 对象被默认初始化或值初始化时自动执行默认构造函数,默认构造函数在以下情形使用:
在块作用域内不使用任何初始值定义一个非静态变量或者数组时
当一个类本身含有类类型的成员且使用合成的默认构造函数时
当类类型的成员没有在构造函数初始值列表中显示初始化时
值初始化在以下情况下发生:
当数组初始化过程中我们提供的初始值数量少于数组大小时
我们不使用初始值定义一个局部静态变量
类必须包含一个默认构造函数以便在上述情况下使用,在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
隐式的类类型转换
● 能通过一个实参调用的构造函数定义了一条从构造函数实参类型到类类型隐式转换的规则
#include<iostream>
#include<string>
using namespace std;
struct Sales_data
{
Sales_data() = default;
explicit Sales_data(const string &s) :bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(istream &);
string isbn()const
{
return bookNo;
}
Sales_data &combine(const Sales_data &);
double avg_price()const;
//数据成员
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum.combine(rhs); //把rhs的数据成员加到sum当中
return sum;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
//输入的交易信息包括ISBN编号、售出总数、售出价格
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
Sales_data::Sales_data(istream &is)
{
read(is, *this);//read 函数的作用是从is中读取一条数据然后存入this对象中
}
double Sales_data::avg_price()const
{
if (units_sold)
{
return revenue / units_sold;
}
else
return 0;
}
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}
int main()
{
Sales_data item;
string null_book = "9-999-99999-9";
//构造一个临时的Sales_data对象
//该对象的units_sold 和 revenue 等于0, bookNo等于null_book
item.combine(null_book);
item.combine("9 - 999 - 99999 - 9"); //错误,需要进行两次转换,第一次是转换成string,再把这个临时的string 转换成 Sales_data
//正确:显示地转换成string,隐式地转换成Sales_data
item.combine(string("9-999-99999-9"));
//正确:显示地转换成Sales_data,隐式地转换成string
item.combine(Sales_data("9-999-99999-9"));
Sales_data item1(null_book); //正确:直接初始化
Sales_data item2 = null_book; //错误:不能将explicit构造函数用于拷贝形式的初始化过程
system("pause");
return 0;
}
● 在上述程序中,有两个构造函数只有一个参数的,它们分别定义了从这两种类型向Sales_data
隐式转换的规则。 也就是说,在需要使用Sales_data
的地方, 我们可以使用 string
或者istream
作为替代
● 在主函数中我们用一个string
实参调用了combine
函数, 该调用是合法的, 编译器用给定的string
自动创建了一个Sales_data
对象。 新生成的这个(临时) Sales_data
对象被传递给combine
。 因为combine
的参数是一个常量引用, 所以我们可以给该参数传递一个临时量。
● 注意: 编译器只会自动地执行一步类型转换。不可以连续使用两种转换机制, 看上述代码主函数
● 关键字explicit
只对一个实参的构造函数有效。 需要多个实参的构造函数不能用于指向隐式转换,所以无须将这些构造函数指定为explicit
的, 只能在类中声明构造函数时使用explicit
关键字, 在类外部定义时不应重复。
● 注意 : 发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。此时我们只能使用直接初始化而不能使用explicit
构造函数(看上述的主函数)
所以当我们使用explicit
关键字声明构造函数时,它将只能以直接初始化的形式使用。 而且,编译器将不会在自动转换过程中使用该构造函数。
为转换显式地使用构造函数
●尽管编译器不会将explicit
的构造函数用于隐式转换过程, 但是我们可以使用这样的构造函数显式地强制进行转换
item.combine(Sales_data(null_book)); //正确,实参是一个显式构造的Sales_data对象
item.combine(static_cast<Sales_data>(cin)); //正确,static_cast可以使用explicit的构造函数
聚合类
● 聚合类使得用户可以直接访问其成员, 当一个类满足如下条件时,我们说它是聚合的:
所有成员都是`public`的
没有定义任何的构造函数
没有类内初始值
没有聚类,也没有`virtual` 函数。
struct Data
{
int ival;
string s;
};
int main()
{
Data vall;
vall.ival = 0;
vall.s = string("Anna");
Data val = { 1,"TT" }; //使用花括号括起来的成员初始值列表,来初始化聚合类的数据成员
Data val = { "TT",1 }; // 错误:初始值的顺序必须与数据成员声明的顺序一致
system("pause");
return 0;
}
● 如果初始值列表中的元素个数少于类的成员数量, 则靠后的成员被值初始化。 初始值列表的元素个数绝不能超过类的成员数量
字面值常量类
● constexpr
函数的参数和返回值必须是字面值类型。字面值类型有: 算术类型、引用和指针。 不过某些类也是字面值类型。 字面值类型的类可能含有constexpr
成员函数。这样的成员必须符号constexpr
函数的所有要求, 它们是隐式 const 的
constexpr 构造函数
class Debug
{
public:
constexpr Debug(bool b = true) :hw(b), io(b),other(b) {}
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) {}
constexpr bool any() const //注意记得在尾部加上const,不然编译错误
{
return(hw || io || other);
}
void set_io(bool b)
{
io = b;
}
void set_hw(bool b)
{
hw = b;
}
void set_other(bool b)
{
other = b;
}
private:
bool hw;
bool io;
bool other;
};
int main()
{
constexpr Debug io_sub(false, true, false);
if (io_sub.any())
{
//一些语句
}
constexpr Debug prod(false);
if (prod.any())
{
//一些语句
}
}
● 构造函数不能是const
的, 但是字面值常量类的构造函数可以是constexpr
函数,该类也必须至少提供一个constexpr
构造函数。
● constexpr
构造函数 可以声明成 =default的形式 或者是删除函数的形式。 否则, constexpr
构造函数就必须既符合构造函数的要求(意味着不能有返回值和返回类型),又符合constexpr
函数的要求(意味着它能拥有的唯一可执行语句就是返回语句)。 constexpr
构造函数体一般来说应该是空的 。
类的静态成员
● 让某些成员与类相关而不是对象:通过在成员声明前加上static使其与类关联在一起,静态成员可以是public
也可以是private
或者是protected
,静态数据成员可以是常量、引用、指针、类类型等 。
class Account
{
public:
void calculate()
{
amount += amount*interestRate; //成员函数不用通过作用域运算符就能直接使用静态成员
}
static double rate()
{
return interestRate;
}
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
static constexpr int period = 30; //period是常量表达式
double daily_tal[period];
};
//一个不带初始值的静态成员的定义
constexpr int Account :: period; //初始值在类的定义中提供
double Account::interestRate = initRate(); /*从类名开始这条定义语句就在类的作用域内了,
所以可以直接使用initRate()函数,
虽然她是私有的,但是可以用来初始化interestRate*/
//注意: 要想确保对象只定义一次,最好把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
void Account::rate(double newRate)
{
interestRate = newRate;
}
int main()
{
double r = Account::rate(); //使用作用域运算符访问静态成员
Account ac1;
Account *ac2 = &ac1;
double c = ac1.rate(); //通过Account的对象或引用调用
double a = ac2->rate(); // 通过指向Account对象的指针
}
● 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。 因此, 每个Account
对象将包含两个数据成员: owner
和 amount
. interestRate
这个数据成员被该类创建所有对象所共享。
● 静态成员函数也不与任何对象绑定在一起,它们不包含this指针,作为结果,静态成员函数不能声明成const
的,而且我们也不能在static
函数体内使用this指针。这一限制既适用于this
的显式使用, 也对调用非静态成员的隐式使用有效。
● 虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用、指针来访问静态成员
● 静态数据成员不属于任何一个对象,她们并不是在创建类的对象时被定义的,它们不是由类的构造函数初始化的,而且一般来说,不能在类的内部初始化静态数据成员,必须在外部定义和初始化每个静态成员。 一个静态数据成员只能定义一次。
● 静态成员数据定义在任何函数之外, 因此一旦它被定义,就将一直存在于程序的整个生命周期中。
静态成员的类内初始化
● 要求静态成员必须是字面值常量类型的constepr,初始值必须是常量表达式;
● 如果在类的内部提供了一个初始值,则成员的定义不能在指定一个初始值了:
//一个不带初始值的静态成员的定义
constexpr int Account :: period; //初始值在类的定义中提供
注意: 既是一个常量静态数据成员在类内被初始化了,通常情况下也应该在类的外部定义一下该成员。
静态成员能用于某些场景,而普通成员不能
class Bar
{
static Bar mem1; //静态成员可以是不完全类型
Bar *mem2; //指针成员可以是不完全类型
Bar mem3; // 数据成员必须是完全类型
};
● 静态数据成员可以是不完全类型,也可以是它所属的类类型。 而非静态数据成员则受到限制,只能声明成它所属类的指针或引用
● 静态成员数据和普通成员数据的另外一个区别是我们可以使用静态成员作为默认实参。
class Screen
{
public:
Screen &clean(char = bkground); //bkground表示一个在类中稍后定义的静态成员
private:
static const char bkground;
};
非静态成员数据不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。
本章完………