C++primer学习心得-第七章-类

31 篇文章 3 订阅

C++primer学习心得-第七章-类

类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现的分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节。

类要像实现数据抽象和封装,需要首先定义一个抽象数据类型。

7.1 定义抽象数据类型

1.设计Sales_data类

这里有提到书店问题,我们需要设计一个类来方便地记录销售记录,需要这个类有一个isbn的成员函数,并且这个类的对象要支持+、-、+=、<<和>>运算符。

自定义运算符的知识是在14章。这里我们先为这些运算定义普通的函数形式。所以这个Sales_data类的接口应该包含下面的操作:

  • 一个isbn成员函数,返回isbn编号。(getter)
  • combine成员函数,类的对象的相加
  • add的函数,执行两个对象的加法
  • read函数,从istream读入到类的对象
  • print函数,类的对象输出到ostream

在我们要考虑怎么设计类的时候不妨考虑这个类应该达到怎样的预期效果,我们能用这个类

Sales_data total;
if(read(cin,total)){
    Sales_data trans;
    while(read(cin,trans)){
        if(total.isbn()==trans.isbn())
            total.combine(trans);
    	else{
        	print(cout,total)<<endl;
            total=trans;
    	}
    }
    print(cout,total)<<endl;
}else{
    cerr<<"No data?"<<endl;
}

2.定义Sales_data类

#include<iostream>
#include<string>

using namespace std;

struct Sales_data {
	string bookNo;               //isbn
	unsigned units_sold = 0;   // 销售量
	double revenue = 0.0;     //总销售额
	string isbn()const { return bookNo; }   
	Sales_data& combine(const Sales_data&rhs){    
		units_sold += rhs.units_sold;
		revenue += rhs.revenue;
		return *this;
	};
	double avg_price() const {
		if (units_sold)
			return revenue / units_sold;
		else
			return 0;
	};
};
Sales_data add(const Sales_data& l, const Sales_data& r) {
	Sales_data sum = l;
	sum.combine(r);
	return sum;
};
ostream& print(ostream& os, const Sales_data& item) {
	os << item.isbn() << " " << item.units_sold << " " << item.revenue
		<< " " << item.avg_price();
	return os;
};
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;
};

int main() {
	Sales_data total;
	if (read(cin, total)) {
		Sales_data trans;
		while (read(cin, trans)) {
			if (total.isbn() == trans.isbn())
				total.combine(trans);
			else {
				print(cout, total) << endl;
				total = trans;
			}
		}
		print(cout, total) << endl;
	}
	else {
		cerr << "No data?" << endl;
	}
}

先把整个代码放出来,需要注意的地方我们再一个一个看。

定义成员函数

首先是定义最简单的成员函数,isbn,做用是返回bookNo(getter),所有的成员必须是在类的内部声明,但成员函数体可以在类内定义也可以在内外定义。

std::string isbn()const {return bookNo;}

this

this是一个常量指针。当类的对象调用成员函数的时候

total.isbn()

实际上是在替某个类调用它。isbn指向类的成员bookNo,则它隐式的指向类的对象的成员。上面的调用实际上隐式的返回total.bookNo。

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象,我们调用一个成员函数时用请求 该函数的对象地址初始化this。编译器负责把total的地址传递给isbn的隐式形参this,可等价的认为编译器将该调用重写了如下形式:

Sales_data::isbn(&total)

其中调用Sales_data的isbn成员时传入了total地址。成员函数内部可以直接使用该对象的成员而无需通过成员访问运算符,因为this正是指向这个对象。任何对类成员的直接访问都被看做this的隐式引用,如isbn使用bookNo时隐式的使用this指向的成员如 this->bookNo一样。

所以我们可以写成以下形式

std::string isbn()const{return this->bookNo;}

const

注意到我们声明成员函数时总会在参数列表后面加一个关键字const,const的作用是修改隐式指针this的类型。默认情况下,this的类型是指向类类型非常量版本的常量指针。尽管this 是隐式的,它仍需遵顼初始化规则,意味着我们不能把this绑定到一个常量对象上。这样我们不能在一个常量对象上调用普通的成员函数。主要地,isbn函数是不会改变对象(成员)的,因此将this设置为指向常量的指针有助于提高函数的灵活性。(只是因为函数不会改变对象成员的,所以我们把this设置成指向常量的指针)。此时的成员函数称为常量成员函数。

类作用域和成员函数

类本身是一个作用域。编译器分两步处理类:先编译成员的声明,然后才是成员函数体。所以成员函数体可以随意使用类的其他成语而不用在意次序。

在类的外部定义成员函数

在外部定义成员函数时成员函数的定义必须域它的声明一致。同时成员的名字必须包含它所属的类名:

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

定义返回this的函数

前面解释了this的含义了,也就不难理解return *this的意义了

Sales_data& Sales_data::combine(const sales_data &rhs){
units_sold+=rhs.units_sold;
    revenue+=rhs.revenue;
    return *this;//返回调用该函数的对象
}

如一个简单的person类

#include<iostream>
#include<string>
using namespace std;

struct person {
	string name;
	string address;
	string getname() const { return name; }
	string getaddress() const { return address; }
};

int main() {
	person a;
	if (cin >> a.name >> a.address)
		cout << a.getname() <<" "<< a.getaddress() << endl;
}

3. 定义类相关的非成员函数

如前面我们定read和print函数

ostream& print(ostream& os, const Sales_data& item) {
	os << item.isbn() << " " << item.units_sold << " " << item.revenue
		<< " " << item.avg_price();
	return os;
};
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 add(const Sales_data& l, const Sales_data& r) {
	Sales_data sum = l;
	sum.combine(r);
	return sum;
};

需要注意的是由于IO类属于不能被拷贝的类型,只能通过引用来传递它们。而且由于读写操作会改变流的内容所以两个函数接受的都是普通引用而非常量引用。

另外print函数不负责换行。

如我们前面的简单的person类添加read和print后

#include<iostream>
#include<string>
using namespace std;

struct person {
	string name;
	string address;
	string getname() const { return name; }
	string getaddress() const { return address; }
};
istream& read(istream& is, person& p) {
	is >> p.name >> p.address;
	return is;
}
ostream& print(ostream& os, const person& p) {
	os << p.name << " " << p.address;
	return os;
}

int main() {
	person a;
	if (read(cin,a))
		print(cout,a)<< endl;

}

4. 构造函数

类通过一个或几个特殊的成员函数(构造函数)来控制其对象的初始化过程。构造函数的任务是初始化类对象的数据成员,只要类的对象被创建就会执行成员函数。

构造函数的名字和类名相同,与其他函数不同的是它没有返回类型。类可以有多个构造函数,与重载函数差不多,不同构造函数之间在参数数量或参数类型上必须要有差别。而且构造函数不能被声明成const的。

合成的默认构造函数

注意到我们前面的类都没有构造函数,但程序仍能够正常的编译运行。我们可以不为这些对象提供初始值,它们会执行默认初始化。此时一个特殊的构造函数–默认构造函数控制默认初始化的过程。默认构造函数无需任何实参 。编译器创建的构造函数又称合成的默认构造函数。

它会按照以下规则初始化类的成员函数:

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

有某些类是不能依赖于合成的默认构造函数的。原因有一些,我就不说了。

我来看看怎么定义构造函数。

对于我们的Sales_data类我们将用下面的四个参数定义4个不同的构造函数:

  • istream&,从中读取一条交易信息
  • const string&,表示isbn号;unsigned,表示售出的图书数量;double,图书的售出价格
  • const string&,isbn号,其他成员将被赋予默认值
  • 空参数列表,默认构造函数。
//构造函数
	Sales_data() = default;
	Sales_data(const string& s) :bookNo(s) {}
	Sales_data(const string& s, unsigned n, double d) :bookNo(s), units_sold(n), revenue(d) {}
	Sales_data(istream&);

首先

Sales_data()=default;

实现它没有接受任何实参,是一个默认构造函数,通过在后面加上**=default**来要求编译器生成构造函数,如果它在类的内部则默认构造函数是内联的,外部则不是内联的。

然后

Sales_data(const string& s) :bookNo(s) {}
Sales_data(const string& s, unsigned n, double d) :bookNo(s), units_sold(n), revenue(d) {}

定义中冒号和花括号之间的部分称为构造函数初始值列表。负责为新创建的对象的一个或数个数据成员赋初值。名字后面跟一个括号括起来的成员初始值。

在类的外部定义构造函数

Sales_data::Sales_data(std::istream &is){read(is,*this);}

以istream为参数的构造器需要执行一些实际的操作。

如下面代码添加构造并使用构造函数

#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 d) :bookNo(s), units_sold(n), revenue(d) {}
	Sales_data(istream&);
	string bookNo;               //isbn
	unsigned units_sold = 0;   // 销售量
	double revenue = 0.0;     //总销售额
	string isbn()const { return bookNo; }   
	Sales_data& combine(const Sales_data&rhs){    
		units_sold += rhs.units_sold;
		revenue += rhs.revenue;
		return *this;
	};
	double avg_price() const {
		if (units_sold)
			return revenue / units_sold;
		else
			return 0;
	};
};
Sales_data add(const Sales_data& l, const Sales_data& r) {
	Sales_data sum = l;
	sum.combine(r);
	return sum;
};
ostream& print(ostream& os, const Sales_data& item) {
	os << item.isbn() << " " << item.units_sold << " " << item.revenue
		<< " " << item.avg_price();
	return os;
};
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); }

int main() {
	Sales_data total;
	Sales_data total1("string");
	Sales_data total2("string", 111, 222);
	Sales_data total3(cin);
	
	print(cout, total) << endl;
	print(cout, total1) << endl;
	print(cout, total2) << endl;
	print(cout, total3) << endl;
	
}


又如我们前面的person类加上构造函数

#include<iostream>
#include<string>
using namespace std;

struct person {
	person() = default;
	person(const string& s, const string& sa) :name(s), address(sa) {}
	person(const string& s) :name(s) {}
	person(istream& is) {
		is >> name >> address;
	}
	string name;
	string address;
	string getname() const { return name; }
	string getaddress() const { return address; }
};
istream& read(istream& is, person& p) {
	is >> p.name >> p.address;
	return is;
}
ostream& print(ostream& os, const person& p) {
	os << p.name << " " << p.address;
	return os;
}

int main() {
	person a;
	person b("jack");
	person c("jackson", "abs");
	person d(cin);
	print(cout, a) << endl;
	print(cout, b) << endl;
	print(cout, c) << endl;
	print(cout, d) << endl;
	if (read(cin,a))
		print(cout,a)<< endl;
}

5. 拷贝、赋值、析构

total=trans;

它的行为等同于下面的代码

total.bookNo=trans.bookNo;
total.units_sold=trans.units_sold;
total.revenue=trans.revenue;

虽然编译器能替我们完成拷贝、赋值、销毁操作,但对于某些类来说合成操作可能不能正常进行。更多信息后面会介绍。

7.2 访问控制与封装

我们使用访问说明符来加强封装性:

  • public :定义在它之后的成员可以在整个程序内被访问
  • private:定义在它之后的成员可以被类的成员函数访问,但不能被使用该类的代码访问
class Sales_data {
public:
	//构造函数
	Sales_data() = default;
	Sales_data(const string& s) :bookNo(s) {}
	Sales_data(const string& s, unsigned n, double d) :bookNo(s), units_sold(n), revenue(d) {}
	Sales_data(istream&);
	Sales_data& combine(const Sales_data&rhs){    
		units_sold += rhs.units_sold;
		revenue += rhs.revenue;
		return *this;
	};
private:
	string bookNo;               //isbn
	unsigned units_sold = 0;   // 销售量
	double revenue = 0.0;     //总销售额
	string isbn()const { return bookNo; }   
	
	double avg_price() const {
		if (units_sold)
			return revenue / units_sold;
		else
			return 0;
	};
};

class和struct的区别前面有说就不提了。

如我们的person类可以这样定义

class person {
public:
	person() = default;
	person(const string& s, const string& sa) :name(s), address(sa) {}
	person(const string& s) :name(s) {}
	person(istream& is) {
		is >> name >> address;
	}
	string getname() const { return name; }
	string getaddress() const { return address; }
private:
	string name;
	string address;

};

1. 友元

前面我们把Sales_data的成员设置成private后马上发现read、print和add函数不能正常编译了,因为他们并不是类的成员。类要允许其他类或函数访问它的非共有成员,只要令其他类或者函数成为它的友元。只需要增加一条以friend开始的函数声明语句即可:

#include<iostream>
#include<string>

using namespace std;

class Sales_data {
	friend ostream& print(ostream&, const Sales_data&);
	friend istream& read(istream&, Sales_data&);
public:
	//构造函数
	Sales_data() = default;
	Sales_data(const string& s) :bookNo(s) {}
	Sales_data(const string& s, unsigned n, double d) :bookNo(s), units_sold(n), revenue(d) {}
	Sales_data(istream&);
	Sales_data& combine(const Sales_data&rhs){    
		units_sold += rhs.units_sold;
		revenue += rhs.revenue;
		return *this;
	};
	double avg_price() const {
		if (units_sold)
			return revenue / units_sold;
		else
			return 0;
	};
	string isbn()const { return bookNo; }   
private:
	string bookNo;               //isbn
	unsigned units_sold = 0;   // 销售量
	double revenue = 0.0;     //总销售额
	
};
Sales_data add(const Sales_data& l, const Sales_data& r) {
	Sales_data sum = l;
	sum.combine(r);
	return sum;
};
ostream& print(ostream& os, const Sales_data& item) {
	os << item.isbn() << " " << item.units_sold << " " << item.revenue
		<< " " << item.avg_price();
	return os;
};
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); }

int main() {
	Sales_data total;
	
	if (read(cin, total)) {
		Sales_data trans;
		while (read(cin, trans)) {
			if (total.isbn() == trans.isbn())
				total.combine(trans);
			else {
				print(cout, total) << endl;
				total = trans;
			}
		}
		print(cout, total) << endl;
	}
	else {
		cerr << "No data?" << endl;
	}
}

如这样字就可以正常编译运行了。

注意友元声明仅仅指定了访问权限,不是通常意义上的函数声明,所以如果希望类的用户能调用某个友元函数,我需要在友元声明外再专门声明一次函数。

再比如我们的person

#include<iostream>
#include<string>
using namespace std;

class person {
	friend istream& read(istream&, person&);
	friend ostream& print(ostream&, const person&);
public:
	person() = default;
	person(const string& s, const string& sa) :name(s), address(sa) {}
	person(const string& s) :name(s) {}
	person(istream& is) {
		is >> name >> address;
	}
	
	string getname() const { return name; }
	string getaddress() const { return address; }
private:
	string name;
	string address;

};
istream& read(istream& is, person& p) {
	is >> p.name >> p.address;
	return is;
}
ostream& print(ostream& os, const person& p) {
	os << p.name << " " << p.address;
	return os;
}

int main() {
	person a;
	if (read(cin,a))
		print(cout,a)<< endl;

}

7.3 类的其他特性

包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、类类型及友元类等。

1.类成员再探

为了展示这一特性,我们需要一对相互关联的类:Screen和Window_mgr。

class Screen{
public:
    typedef std::string::size_type pos;
private:
    pos cursor=0;
    pos height=0,width=0;
    std::string contents;
}

以上使用了typedef,也可以使用类型别名

using pos=std::string::size_type;

定义类型的成员必须先定义后使用。

我的Screen类的定义如下

class Screen {
public:
    typedef std::string::size_type pos;
    Screen() = default;
    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声明成员函数,也可以在类的外部用关键字inline修饰函数的定义。

成员函数也可以被重载。

有时我们需要在const对象内修改类的某个成员,可以通过在变量中加入mutable关键字。

一个可变数据成员永远不会是const,即使他是const对象的成员。所以一个const成员函数可以改变一个可变成员的值。

然后我们需要定义一个Window_mgr类,它将包含一个Screen类的vector,每个元素表示一个特定的Screen。

class Window_mgr{
    private:
    std::vector<Screen> screens{Screen(24,80," ")};
};

2.返回*this的成员函数

如我们的Screen类定义为

class Screen {
public:
    typedef std::string::size_type pos;
    Screen() = default;
    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);
    Screen& set(char);
    Screen& set(pos, pos, char);
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;
}
inline char Screen::get(pos r, pos c)const {
    pos row = r * width;
    return contents[row + c];
}
inline Screen& Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch) {
    contents[r * width + col] = ch;
    return *this;
}

此时,我们可以把一系列的操作连接在一条表达式中

myScreen.move(4,0).set('#')

等价于

myScreen.move(4,0);
myScreen.set('#');

注意move和set的返回类型是Screen&而非Screen,如果是Screen,则返回值将是*this的一个副本。

接下来我们需要定义一个display函数来打印Screen的内容。由于它不会改变对象的内容,我们将他设置为const的,但如果设置为const的,this将是一个指向const的指针,*this是const对象,它的返回类型就是const Sales_data&。这样的话我们就不能把他嵌入到一组动作序列中去了。

这时我们可以通过区分成员函数是否是const的来对其进行重载。

最终如下

class Screen {
public:
    typedef std::string::size_type pos;
    Screen() = default;
    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);
    Screen& set(char);
    Screen& set(pos, pos, char);

    Screen& display(ostream& os) {
        do_display(os);
        return *this;
    }
    const Screen& display(ostream& os) const{
        do_display(os);
        return *this;
    }
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
    void do_display(ostream& os)const { os << contents; }
};

3. 类类型

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

Sales_data item1;
class Sales_data item1;

类的声明和定义也可以分开

class Screen;//Screen类的声明

这种声明也称为前向声明。在定义之前它是不完全类型,只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,可以声明以不完全类型作为参数或返回类型的函数。

因此类允许包含指向它自身类型的引用或指针:

class Link_screen{
    Screen window;
    Link_screen *next;
    Link_screen *prev;
};

理解了这些内容后这小节的练习题也就很简单了

练习:定义一对类X和Y,其中X包含一个指向Y的指针,Y包含一个类型为X的对象。

#include<iostream>
class Y;//思路是先声明Y再定义X再定义Y
class X {
	Y* y;
};
class Y {
	X x;
};

4. 友元再探

前面我们已经介绍了类的非成员的外部定义的函数要想访问类的私有成员需要在类内部声明为友元。现在我们来考虑类之间的友元。

类之间的友元

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

class Screen{
    friend class Window_mgr;
}
class Window_mgr{
public:
    using ScreenIndex = vector<Screen>::size_type;
    void clear(ScreenIndex);
private:
    vector<Screen> screens{ Screen(24,80,' ') };
};
void Window_mgr::clear(ScreenIndex i) {
    Screen& s = screens[i];
    s.contents = string(s.height * s.width, ' ');
}

令其他类的成员函数作为友元

我们还可以令Window_mgr的成员函数clear成为Screen的友元,这样Screen只为clear提供访问权限。

class Screen{
friend void Window_mgr::clear(ScreenIndex)
};

但对于这方面是很严格,我们必须按以下方式来设计程序:

  • 我们首先定义Window_mgr类,其中只声明clear函数但不定义。在定义clear前必须先声明Screen。
  • 然后定义Screen,包括声明clear为友元。
  • 最后定义clear类,此时才可以使用Screen成员。

之前定义类为友元时,我们只需要先定义Screen再定义Window_mgr就行了,也不会出现问题,我们也不需要提前声明Window_mgr类,这两种情况的差异可能是c++的编译机制造成的。

另外,一个类要想把一组重载函数声明成它的友元需要对这组函数中的每一个都分别声明。

如下面代码通过调整各代码块的顺序将clear函数作为Screen的友元,最终正常编译运行了

#include<iostream>
#include<string>
#include<vector>
using namespace std;

class Screen;
class Window_mgr {
public:
    using ScreenIndex = vector<Screen>::size_type;
    void clear(ScreenIndex);
private:
    vector<Screen> screens;//{ Screen(24,80,' ') }
};
class Screen {
    //friend class Window_mgr;
    friend void Window_mgr::clear(ScreenIndex);
public:
    typedef std::string::size_type pos;
    Screen() = default;
    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);
    Screen& set(char);
    Screen& set(pos, pos, char);
    Screen& display(ostream& os) {
        do_display(os);
        return *this;
    }
    const Screen& display(ostream& os) const{
        do_display(os);
        return *this;
    }
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
    void do_display(ostream& os)const { os << contents; }
};
inline Screen& Screen::move(pos r, pos c) {
    pos row = r * width;
    cursor = row + c;
    return *this;
}
inline char Screen::get(pos r, pos c)const {
    pos row = r * width;
    return contents[row + c];
}
inline Screen& Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch) {
    contents[r * width + col] = ch;
    return *this;
}

void Window_mgr::clear(ScreenIndex i) {
    screens = { Screen(24, 80, ' ') };
    Screen& s = screens[i];
    s.contents = string(s.height * s.width, ' ');
}
int main() {
    Screen myscreen(5, 5, 'X');
    myscreen.move(4, 0).set('#').display(cout);
    cout << '\n';
    myscreen.display(cout);
    cout << '\n';
}

7.4 类的作用域

每个类都会定义它自己的作用域。

如我们为Window_mgr添加一个addScreen的函数负责向显示器添加一个新的屏幕:

class Window_mgr {
public:
    using ScreenIndex = vector<Screen>::size_type;
    void clear(ScreenIndex);
    ScreenIndex addScreen(const Screen&);
private:
    vector<Screen> screens;//{ Screen(24,80,' ') }
};
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen& s) {
    screens.push_back(s);
    return screens.size() - 1;
}

1.名字查找与类的作用域

到现在我们对名字查找的过程也有所体会了:

  • 现在名字所在的块中查找其声明语句(名字使用之前的)
  • 若没找到则考虑在外层作用域中找(也是名字使用前的)
  • 若还是没找到则报错

类的定义分为两步:

  • 编译成员的声明
  • 之后编译函数体

注意类型名需要特殊对待。如果成员使用了外层作用域中的某个名字(类型),则类不能在之后重新定义该名字。

然后成员函数中使用的名字按照以下方式解析:

  • 先下函数内找该名字的声明
  • 若没找到,在类中继续找
  • 若没找到,在外层作用域中找(成员函数定义前的)

7.5 构造函数再探

1.构造函数初始值列表

需要注意构造函数中的初始化与赋值的区别

有时我们可以采用以下的构造函数-赋值的方式

Sales_data::Sales_data(const string &s,unsigned cnt,double price){
    bookNo=s;
    units_sold=cnt;
    revenue=cnt*price;
}

这样做有时没问题,但有时不行,有时必须初始化而不能赋值

class constRef{
    public:
    	constRef(int ii);
    privete:
    	int i;
    	const int ci;
    	int &ri;
}

对于这个类的构造函数必须使用初始化。

constRef::constRef(int ii)::i(ii),ci(ii),ri(i){}

初始化的顺序也是需要注意的。构造函数初始值列表中的数据成员顺序最好与这些成员的声明顺序一直,否则容易出错。

另外构造器函数的形参列表可以接受默认实参。

2.委托构造函数

委托构造函数使用类的其他构造函数来执行它自己的 初始化过程,如

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);}
};

3.默认构造函数的作用

4. 隐式的类类型转换

5.聚合类

聚合类(aggregate class)使用户可以直接访问其成员,且具有特殊的初始化语法形式。它应满足以下条件:

  • 所有成员都是pubic的
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数(第15章介绍)

struct Data{
    int ival;
    string s;
};

我们可以这样初始化聚合类的成员

//vall.ival=0;vall.s=string("Anna")
Data vall={0,"Anna"};

要注意初始值的顺序。

6.字面值常量类

数据成员都是字面值类型的聚合类是字面值常量类,如不是聚合类,要是字面值常量类就必须满足下面要求:

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

7.6 类的静态成员

通过加static声明静态成员。

静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据,

静态成员函数也不与任何对象绑定,他们不包含this指针。所以静态成员函数不能声明为const的,函数体也不能使用this指针。

虽然静态成员不属于某个对象,但我们仍可以使用类的对象、引用、指针来访问静态成员。(Java中也有类似的特性,有时会因此陷入误区)。成员函数不用作用域运算符就能直接访问静态成员。

注意static只能存在类的内部的声明语句,外部定义静态成员时不要static关键字。

通常我们不能再类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员。

但我们可以为const整数类型的静态成员提供类内初始值,初始值必须是constexpr的。

注意 :静态数据成员可以是不完全类型(只声明而未完全定义的类)。


另外将Screen和Window_mgr两个类分别作为头文件的话要使程序正常编译运行只需要在之前的基础上稍作修改

Window_mgr.h

#ifndef WINDOW_MGR
#define WINDOW_MGR

#include<vector>
class Screen;
class Window_mgr {
public:
    using ScreenIndex =std:: vector<Screen>::size_type;
    void clear(ScreenIndex);
    ScreenIndex addScreen(const Screen&);
private:
    std::vector<Screen> screens;//{ Screen(24,80,' ') }
};
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen& s) {
    screens.push_back(s);
    return screens.size() - 1;
}
#endif // !WINDOW_MGR

Screen.h

#ifndef SCREEN_H
#define SCREEN_H
#include<string>
#include<iostream>

class Screen {
    //friend class Window_mgr;
    friend void Window_mgr::clear(ScreenIndex);

public:
    typedef std::string::size_type pos;
    Screen() = default;
    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);
    Screen& set(char);
    Screen& set(pos, pos, char);

    Screen& display(std::ostream& os) {
        do_display(os);
        return *this;
    }
    const Screen& display(std::ostream& os) const {
        do_display(os);
        return *this;
    }

private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
    void do_display(std::ostream& os)const { os << contents; }
};
#endif

main.cpp

#include<iostream>
#include<string>
#include<vector>
#include "Window_mgr.h"
#include "Screen.h"

using namespace std;

inline Screen& Screen::move(pos r, pos c) {
    pos row = r * width;
    cursor = row + c;
    return *this;
}
inline char Screen::get(pos r, pos c)const {
    pos row = r * width;
    return contents[row + c];
}
inline Screen& Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch) {
    contents[r * width + col] = ch;
    return *this;
}
void Window_mgr::clear(ScreenIndex i) {
    screens = { Screen(24, 80, ' ') };
    Screen& s = screens[i];
    s.contents = string(s.height * s.width, ' ');
}

int main() {
    Screen myscreen(5, 5, 'X');
    myscreen.move(4, 0).set('#').display(cout);
    cout << '\n';
    myscreen.display(cout);
    cout << '\n';
}

稍微修改得到的另一种Screen类

#include<iostream>
#include<string>
using namespace std;

class Screen {
public:
	typedef string::size_type sz;
	//构造函数
	//Screen() = default;
	Screen(sz w, sz h, char ch) :width(w), height(h), content(w* h, ch) {}
	Screen() :Screen(0, 0, ' ') {}
	Screen(sz w, sz h) :Screen(w, h, ' ') {}
	Screen& move(sz r, sz c) {//移动光标,行与列
		cursor =r*width + c;
		return *this;
	}
	char get(sz r, sz c) { return content[r * width + c]; }//得到某行某列的字符
	Screen& set(char c) {//设置光标处的字符为给定字符
		content[cursor] = c;
		return *this;
	}
	Screen& set(sz r, sz c, char ch) {//设置某个位置的字符为给定字符
		content[r * width + c] = ch;
		return *this;
	}
	Screen& display(ostream &os=cout) {
		do_display(os);
		return *this;
	}
	const Screen& display(ostream& os=cout) const{
		do_display(os);
		return *this;
	}
	Screen& next(ostream& os=cout) {
		string str = string(width, '-');
		os <<"\n"<< str << endl;
		return *this;
	}
private:
	sz cursor=0;
	sz width, height;
	string content;
	void do_display(ostream &os) const{ //输出,打印操作
		for (int i = 0; i < content.size(); i++) {
			if (i % width == 0)
				os << "\n";
			os << content[i];
		}
		//os << content;
	}
};

int main() {
	int w = 120, h =25;
	char ch = 'o';
	Screen window(w,h,ch);
	window.display().next();
	for (int i = 0; i < h; i++)
		window.set(i, i, ' ').display().next();
	window.display();

}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值