《C++ Primer》第七章 类

《C++ Primer》第七章 类

7.1 定义抽象数据类型

类的基本思想是数据抽象与封装。数据抽象是一种以利于接口和实现分离的变成技术。要想实现数据抽象与封装,需要先定义一个抽象数据类型。

类内:

this: 总是指向"这个"对象,是一个常量指针。不允许改变this中保存的地址。

const: 修改隐式this指针的类型。默认情况下,this的类型时指向类类型非常量版本的常量指针。

常量成员函数:紧跟在参数列表后面的const表示this是一个指向常量 的指针。

//下面代码是非法的,因为不能显示地定义自己的this指针
//此处的this指针是一个指向常量的指针,因为isbn是一个常量成员
std::string  Sales_data::isbn(const Sales_data *const this)
{	return this->isbn;	}

this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容。在上例中,isbn可以读取对象的数据成员,但不能写入值。

常量对象,以及常量对象的引用或指针都只能调用常量成员函数

如果成员被声明成常量成员函数,那么它的定义必须在参数列表后明确const属性。同时,类外部定义的成员的名字必须包含它所属的类名:

double Sales_data::avg_price() const{
    if(units_sold)
        return revenue/units_sold;
    else
        return 0;
}

定义一个返回this对象的函数:调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象则通过显示的实参被传入参数。

Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold;//把rhs的成员加到this对象的成员上
    revenue += rhs.revenue;
    return *this;           //返回调用该函数的对象 total的引用
}

当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了保持一致,combine函数必须返回引用类型。因为此时的左侧运算对象时一个Sales_data对象,所以返回类型应该时Sales_data&。

定义类相关的非成员函数:从概念上将某些函数属于类的接口函数,但实际上不属于类本身。通常把函数的定义与声明分开来。

定义read和print函数:

//输入的交易信息包括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;
}
ostream &print(ostream &os, const Sales_data &item)
{
	os<<item.isbn()<<" "<<item.units_sold<<" "
	  <<item.revenue<<" "<<item.avg_price();
	return os;
}

read函数:从给定流中将数据读到给定的对象里,print函数则负责将给定对象的内容打印到给定的流中。因为读取和写入的操作会改变流的内容,所以两个函数接受的都是普通引用,而非对常量的引用。

定义add函数:接受两个Sales_data对象作为其参数,返回值是一个新的Sales_data,用于表示前两个对象的和:

Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum=lhs;
	sum.combine(rhs); //把rhs的数据成员加到sum当中
	return sum;
}

构造函数:类通过一个或几个特殊的成员函数来控制其对象的初始化过程。其任务时初始化对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。构造函数没有返回类型,此外,构造函数也有一个(可能为空)的参数列表和一个(可能为空的)函数体。默认构造函数无需任何实参。

初始化类的数据成员:

  • 如果存在类内的初始值,则用它来初始化成员。
  • 否则,默认初始化该成员。

某些类不能依赖于合成的默认构造函数:

  • 编译器只有在发现类不包含任何构造函数的情况下才会生成一个默认构造函数。一旦自定义构造函数,除非再定义一个默认的构造函数,否则类将没有构造函数。
  • 对于某些类来说,合理的默认构造函数可能执行错误的操作。如定义在块中的内置类型或复合类型(如数组和指针)的对象被默认初始化,那么它们的值将是未定义的。
  • 有时候编译器不能为某些类合成默认构造函数。如类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数。
struct Sales_data {
	Sales_data() = default;// =default既可以出现在类内部,也可以作为定义出现在类外部
	Sales_data(const std::string &s): bookNo(s){}
	Sales_data(const std::string &s,unsigned n, double p):
			//构造函数初始值列表
			bookNo(s), units_sold(n), revenue(p*n) {}
	Sales_data(std::istream &);
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

在类外部定义构造函数:

Sales_data::Sales_data(std::istream &is)
{
	read(is,*this);//从is中读取一条交易信息然后存入this对象中
}

7.2 访问控制与封装

在C++中,使用访问说明符加强类的封装性。

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(隐藏了)类的实现细节。
class Sales_data{
public:
	Sales_data() = default;
	Sales_data(const string &s, unsigned n, double p):
				bookNo(s), units_sold(n),revenue(p*n){}
	Sales_data(const string &s):bookNo(s){}
	Sales_data(istream&);
	string isbn() const { return bookNo; }
	Sales_data &combine(const Sales_data&);
private:
	double avg_price() const
		{ return units_sold ? revenue/units_sold : 0; }
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

struct和class的区别:

使用srtuct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果使用class关键字,则这些成员是private的。

友元:类可以允许其他类或者函数访问它的非公用成员,方法是令其他类或者函数成为友元。增加一条以friend关键字开始的函数声明语句即可。

//Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);

友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。

封装优点:

  • 确保用户代码不会无意间破坏对象的状态。
  • 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

7.3 类的其它特性

自定义某种类型在类中的别名,一样存在访问限制:用定义类型的成员必须先定义再使用,这一点与普通成员有所区别。

class Screen{
public:
	typedef std::string::size_type pos;
    //也可以使用类型别名等价声明一个类型名字
    //using pos  = std::string::size_type;
private:
	pos cursor=0;
	pos height=0,width=0;
	std::string contents;
};

Screen类的成员函数以及内联函数:

class Screen{
public:
	typedef std::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 ht, pos wd) const;	//显式内联
	Screen &move(pos r, pos c);				//能在之后设为内联
private:
	pos cursor=0;
	pos height=0,width=0;
	std::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];	//返回给定的字符
}

重载成员函数:只要函数之间在参数的数量和类型上有所区别就行

Screen myscreen;
char ch=myscreen.get();		//调用Screen::get()
ch = myscreen.get(0,0);		//调用Screen::get(pos,pos)

可变数据成员:永远不会是const,即使它是const对象的成员。一个const成员函数可以改变一个可变成员的值。例如:给Screen添加一个名为access_ctr的可变成员,通过它我们可以追踪每个Screen的成员函数被调用了多少次。

class Screen{
public:
	void some_member() const;
private:
	mutable size_t access_ctr;	//即使在一个const对象内也能被修改
};
void Screen::some_member() const
{
	++access_ctr;	//保存一个计数值,用来记录成员函数被调用的次数
}

返回*this的成员函数:返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。

一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

基于const的重载:通过区分成员函数是否是const的,我们可以对其进行重载。因为非常量版本的函数对于常量对象是不可用的,所以只能在一个常量对象上调用const成员函数。

class Screen{
public:
	//根据对象是否是const重载了display函数
	Screen &display(std::ostream &os)
					{ do_display(os); return *this; }
	const Screem &display(std::ostream &os) const
					{ do_display(os); return *this; }
private:
	void do_display(std::ostream &os) const {os<<contents;}
}

Screem myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(cout);		//调用非常量版本
blank.display(cout);					//调用常量版本

类类型:每个类定义了唯一的类型,对于两个类而言,即使它们的成员完全一样,这两个类也是不同的类型。

struct First{
	int memi;
	int getMem();
};
struct Second{
	int memi;
	int getMem();
};
First obj1;
Second obj2 = obj1;	//错误:obj1和obj2类型不同

我们可以将类名作为类型的名字使用,从而指向类类。或者把类名跟在关键字class或struct后面。

Sales_data item1;
class Sales_data item1;//等价的声明

类的声明:可以仅声明类而暂时不定义它。这种声明有时被叫做前向声明。

友元类:如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非共有成员在内的所有成员。

class Screen{
	friend class Window_mgr;
};

class Window_mgr{
public:
	//窗中每个屏幕的编号
	using ScreenIndex = std::vector<Screen>::size_type;
	//按照编号将指定的Screen重置为空白
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24, 80 ,' ')};
};
void Window_mgr::clear(ScreenIndex i)
{
	//s是一个Screen的引用,指向想清空的屏幕
	Screen &s=screen[i];
	//将选定的Screen重置为空白
	s.contents = string(s.height*s.width, ' ');
}

令成员函数作为友元:当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类。

class Screen{
	//Window_mgr::clear必须在Screen类之前被声明
	friend void Window_mgr::clear(ScreenIndex);
};

函数重载与友元:尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数的每一个分别声明。

//重载的storeOn函数
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen{
	//storeOn 的 ostream 版本能访问Sreen对象的私有部分
	friend std::ostream& storeOn(std::ostream &,Screen &);
};

接受BitMap&作为参数的版本仍然不能访问Screen。

友元声明和作用域:用声明友元的类的成员调用友元函数,这个友元函数也必须是声明过的:

struct X{
	friend void f(){/*友元函数可以定义在类的内部*/}
	X() {f();}			//错误:f还没有被声明
	void g();
	void h();
};
void X::g() {return f();}		//错误:f还没有被声明
void f();						//声明那个定义在X中的函数
void X::h() {return f();}		//正确:f的声明在作用域了

7.4 类的作用域

在类的作用域外,普通的数据和函数成员只能有对象、引用或者指针使用成员访问运算符来访问。

作用域和定义在类外部的成员:

  • 在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。
  • 函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这是,返回类型必须指明它是哪个类的成员。
class Window_mgr {
public:
	//向窗口添加一个Screen,返回它的编号
	ScreenIndex addScreen(const Screen&);
};
//首先处理返回类型,之后才进入Window_mgr的作用域
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s)
{
	screen.push_back(s);
	return screens.size()-1;
}

因为返回类型出现在类名之前,所以事实上它是位于Window_mgr类的作用域之外的。在这种情况下,要想用ScreenIndex作为返回类型,必须明确定义哪个类定义了它。

编译器处理完类中的全部声明后才会处理成员函数的定义。

用于类成员声明的名字查找:如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找:

typedef double Money;
string bal;
class Account{
public:
	Money balance(){ return bal; }
private:
	Money bal;
};

类型名要特殊处理:一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。

typedef double Money;
class Account{
public:
	Money balance() { return bal; }	//使用外层作用域的Money
private:
	typedef double Money;	//错误:不能重新定义Money
	Money val;
};

注意:即使Account中定义的Money类型与外层作用域一致,上述代码仍然错误。

成员定义在普通块作用域的名字查找:

如果类的成员被隐藏了,我们仍然可以通过加上类的名字或显式地使用this指针来强制访问成员。

类作用域之后,在外围的作用域中查找:

尽管外层的对象被隐藏,但我们仍然可以通过作用域运算符访问它。

void Screen::dummy_fcn(pos height){
	cursor = width* ::height;		//使用全局的height
}

在文件中名字的出现处对齐进行解析:当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。

int height;
class Screen{
public:
	typedef std::string::size_type pos;
	void setHeight(pos);
	pos height = 0;	//隐藏了外层作用域中的height
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var){
	//var:参数
	//height:类的成员
	//verify:全局函数
	height = verify(var);
}

7.5 构造函数

构造函数初始值列表:如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。

构造函数的初始值有时必不可少:如果成员时const或者引用的话,则必须将其初始化。当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。如果成员时const、引用或者属于某种未提供默认构造函数的类类型,则必须通过构造函数初始值列表为这些成员提供初值。

class ConstRef{
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;
};

//成员ci和ri必须初始化
ConstRef::ConstRef(int ii)
{	//赋值
	i = ii;		//正确
	ci = ii;	//错误:不能给const赋值
	ri = i;		//错误:ri没被初始化
}

//正确:显式地初始化引用和const成员
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}

成员初始化的顺序:如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键。

class X{
	int i;
	int j;
public:
	//未定义的:i在j之前被初始化
	X(int val):j(val),i(j){}
};

实际上:i先被初始化,因此这个初始值的效果是试图使用未定义的值j初始化i!。

最好令构造函数初始值的顺序与成员声明的顺序保持一致,如果可能的话,尽量避免使用某些成员初始化其他成员。

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

委托构造函数:一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程。

class Sales_data{
public:
	//非委托构造函数使用对应的实参初始化成员
	Sales_data(std::string s, unsigned cnt, double price):
			bookNo(s),units_sold(cnt),revenue(cnt*price){}
	//其余构造函数全都委托给另一个构造函数
	Sales_data():Sales_data("",0,0){}
	Sales_data(std::string s):Sales_data(s,0,0){}
	Sales_data(std::istream &is): Sales_data()
				{ read(is,*this); }
};

默认构造函数的作用:当对象被默认初始化或值初始化时自动执行默认构造函数。

使用默认构造函数:

Sales_data obj();		//obj实际的含义是一个不接受任何参数的函数并且返回值是Sales_data类型的对象
if (obj.isbn() == Primer_5th_ed.isbn())		//错误:obj是一个函数

//如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空的括号对
Sales_data obj;	//obj是个默认初始化的对象

隐式的类类型转换:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的 隐式转换机制。

只允许一步一类类型转换:编译器只会自动执行一步类型转换。

//错误:需要用户定义的两种转换
//把“9999999” 转换为string
//再把这个临时的string转换成Sales_data
item.combine("99999999")
//正确:显式地转换成string,隐式地转换成Sales_data
item.combine(string("9999999"))
//正确:隐式地转换成string,显式地转换成Sales_data
item.combine(Sales_data("9999999"))

抑制构造函数定义的隐式转换

再要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:关键字explicit只对一个实参的构造函数有效。explicit构造函数只能用于直接初始化。

class Sales_data {
public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p):
				bookNo(s),units_sold(n),revenue(p*n){ }
	explicit Sales_data(const std::string &s): bookNo(s){}
	explicit Sales_data(std::istream&);
};

//此时,没有任何构造函数能用于隐式地创建Sales_data对象,之前的两种用法都无法通过编译
item.combine(null_book);	//错误:string构造函数是explicit的
item.combine(cin);			//错误:istream构造函数是explicit的

//错误:explicit关键字只允许出现再类内的构造函数声明处
explicit Sales_data::Sales_data(istream& is)
{
    read(is,*this);
}

Sales_data item1(null_book);	//正确:直接初始化
//错误:不能将explicit构造函数用于拷贝形式的初始化过程
Sales_data item2=null_book;

//正确:实参是一个显式构造的Sales_data对象
item.combine(Sales_data(null_book));
//正确:static_cast可以使用explicit的构造函数
item.combine(static_cast<Sales_data>(cin));

聚合类:使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它时聚合的:

  • 所有成员都是public的
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数。
struct Data{	//聚合类
	int ival;
	string s;
};
//可以提供一个花括号括起来的成员初始值列表,用它初始化聚合类的数据成员
Data vall = {0,"Anna"};
//错误:初始值的顺序必须与声明的顺序一致
Data val2 = {"Anna",0};

显式初始化类的对象成员存在3个缺点:

  • 要求类的成员都是public的
  • 将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)
  • 添加或删除一个成员后,所有的初始化语句都需要更新

字面值常量类:

数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合以下要求,则它也是一个字面值常量类:

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个constexpr 构造参数
  • 如果一个数据成员含有类内初始值,则内置成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr 构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr 构造函数

尽管构造函数不能是const 的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。constexpr 构造函数体一般是空的。我们通过前置关键字constexpr就可以声明一个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() { return hw||io||other; }
	void set_io(bool b)  { io=b; }
	void set_hw(bool b)  { hw=b; }
	void set_other(bool b) { hw=b; }
private:
	bool hw;	//硬件错误,非IO错误
	bool io;	//IO错误
	bool other;	//其他错误
};

constexpr Debug io_sub(false,true,false);	//调试IO
if(io_sub.any())	//等价于if(true)
	cerr << "print appropriate error messages" <<endl;
constexpr Debug prod(false);	//无调试
if(prod.any())					//等价于if(false)
	cerr << "print an error message"<<endl;

7.6 类的静态成员

声明静态成员:在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以是public或private的。静态数据成员的类型可以是常量、引用、指针、类类型等等。

class Account{
public:
	void calculate() { amount+=amount*interestRate; }
	static double rate() { return interestRate; }
	static void rate(double);
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate();
};

静态成员不和任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能被声明成const的而且我们也不能在static函数体内使用this指针。

使用类的静态成员:使用作用域运算符直接访问静态成员;成员函数不能通过作用域运算符就能直接使用静态成员。

double r;
r = Account::rate(); 	//使用作用域运算符访问静态成员

虽然静态成员不属于类的某个对象,但是我们仍然可以使用类、引用或者指针来访问静态成员:

Account ac1;
Account *ac2 = &ac1;
//调用静态成员函数rate的等价形式
r = ac1.rate();			//通过指向Account的对象的引用
r = ac2.rate();			//通过指向Account对象的指针

和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。static关键字则只能出现在类内部的声明语句中。

void Account::rate(double newRate)
{
	interestRate = newRate;
}

//定义并初始化一个静态成员
double Account::interestRate = initRate()

静态成员的类内初始化:一般类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式。

class Account{
public:
	static double rate() { return interestRate; }
	static void rate(double);
private:
	static constexpr int period = 30; //period是常量表达式
 	double daily_tbl[period];
};

静态成员能用于某些常量,而普通成员不行:静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制。

class Bar{

private:
	static Bar mem1;	//正确:静态成员可以是不完全类型
	Bar *mem2;			//正确:指针成员可以是不完全类型
	Bar mem3;			//错误:数据成员必须是完全类型
};

我们可以使用静态成员作为默认实参,非静态成员不能作为默认实参,因为它的值本身就属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。

class Screen{
public:
	//bkground表示一个在类中稍后定义的静态成员
	Screen& clear(char = bkground);
private:
	static const char background;
};

7.7总结

class和struct的区别:默认访问权限不同。

  • struct: 定义在第一个访问说明符之前的成员都是public的
  • class: 定义在第一个访问说明符之前的成员都是private的

封装的含义和用处

  • 封装是指保护类的成员不被随意访问的能力,通过把类的实现细节设置为private,我们就能实现类的封装。封装实现了类的接口和实现的分离。
  • 封装优点1:确保用户代码不会无意间破坏封装对象的状态
  • 封装优点2:被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码

友元在什么时候用?指出其利弊

  • 友元为类的非成员接口函数提供了访问其私有成员的能力
  • 当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。
  • 此时,友元可以“工作在类的内部”,像类的成员一样访问类的所有数据和函数。
  • 但一旦使用不慎(比如随意设定友元),就可能破坏类的封装性。

函数的返回值如果是引用,则表明函数返回的是对象本身;函数的返回值如果不是引用,则表明函数返回的是对象的副本。

如果接受string的构造函数和接受istream&的构造函数都是用默认实参,这种行为合法吗?如果不,为什么?

如果为构造函数的全部形参都提供了默认实参,则该构造函数同时具备了默认构造函数的作用。即使不提供任何实参创建类的对象,也可以找到可用的构造函数。如果为两个构造函数都赋予了默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦不提供任何实参地创建类的对象,则编译器无法判断这两个重载的构造函数哪个更好,从而出现二义性错误。

对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和复合类型的成员只对定义在全局作用域中的对象执行初始化。

使用explicit的优缺点:explicit用于抑制类类型的隐式转换

  • 优点是避免因隐式类类型转换而带来意想不到的错误
  • 缺点是当用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现

vector将其单参数的构造函数定义成explicit的,而string则不是,为什么?

  • string接受的单参数是const char*类型,如果我们得到了一个常量字符指针(字符数组),则把它看作string对象是自然而然的国债,编译器自动把参数类型转换成类类型也非常符合逻辑,因此无需指定为explicit的。
  • vector接受的单参数是int类型,这个参数指定vector的容量。如果我们在本来需要vector的地方提供一个int值并且希望这个int值自动转换成vector,则这个过程比价牵强,因此把vector的单参数构造函数定义成explicit的更合理。

什么是类的静态成员?有何优点?静态成员与普通成员有何区别?

  • 静态成员是指声明语句之前带有关键字static的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享
  • 优点:作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;可以有私有成员,而全局对象不可以
  • 区别体现在普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。静态成员可以作为默认实参,而普通数据成员不能作为默认实参。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1100dp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值