阿里云服务器开始学习Linux
学了一些C++语法后,像尝试在Linux下学习编程,所以注册了阿里云服务器来开始学习Linux的
下面是最近做的C++笔记
Chapter 7 类
定义抽象数据类型
设计Sales_data类
空
定义改进的Sales_data类
struct Sales_data { 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 add(const Sales_data&, const Sales_data&); std::ostream& print(std::ostream&, const Sales_data&); std::istream& read(std::istream&, const Sales_data&);
定义在类内部的函数是隐式的inline函数
定义成员函数
成员函数可以定义在类内,也可定义在类外
std::string isbn() const { return bookNo; }
引入this
this是一个常量指针,不允许改变this保存的指针
成员函数通过调用this的额外的隐式参数来访问调用它的那个对象,当调用一个成员函数时,用请求该函数的对象地址初始化this。eg:
total.isbn();
编译器负责把total的地址传递给isbn的隐式形参this。
在成员函数内部,可以直接调用该函数的对象的成员,无须通过成员访问运算符。this所指的就是这个对象。任何对类成员的直接访问都被看作是this的隐式引用
std::string isbn() const { return bookNo; }
上述语句等价于:
std::string isbn() const { return this->bookNo; }
引入const成员函数
const成员函数:在成员函数参数列表之后的添加const关键字
Type functionName() const { function body }
默认情况下,this的类型是指向类类型非常量版本的常量指针
std::string isbn() const { return bookNo; }
此处从const的作用是修饰隐式的this指针的类型
默认情况下,不能把this绑定在一个常量对象上,因此不能在一个常量对象上调用普通的成员函数
若isbn是一个普通函数而且this是一个普通的指针参数,则应把this声明为const Sales_data *const
this是隐式的且不会出现在参数列表中.
把const关键字放在成员函数的参数列表之后表示this是一个指向常量的指针.像这样使用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;//返回调用该函数的对象,解引用this指针获得执行该函数的对象 }
调用combine函数
total.combine(trans);
内置的赋值运算符把他的左侧运算对象当成左值返回,因此为与之保持一致,combine函数必须返回引用类型.
因为此时的左侧运算对象是一个Sales_data的对象(此处是total这个对象),故返回类型应该是Sales_data&
定义类相关的非成员函数
通常把函数的声明和定义分离开来
如果函数属于类但不定义在类内,则一般应类声明(而非定义)在同一个头文件中.
一般来说,若非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内
IO类属于不能被拷贝的类型,只能通过引用来传递它们
因为读取和写入的操作会改变流的内容,所以两个函数接受的都是普通引用而不是对常量的引用
构造函数
用于控制对象初始化过程的成员函数,称为构造函数
构造函数无返回类型
类可有多个构造函数,与重载函数 差不多
构造函数不能被声明成const的
创建类的一个const对象时,直到构造函数完成初始化过程,对象才真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值
合成的默认构造函数
类通过默认构造函数来控制默认初始化过程,默认构造函数无须任何实参
当类没有显式的定义构造函数,编译器会隐式地定义一个默认构造函数,又称为合成的默认构造函数
对于大多数类来说,合成的默认构造函数按照如下规则初始化类的数据成员
- 如果存在类内的初始值,用它来初始化成员。
- 否则,默认初始化该成员
某些类不能依赖于合成的默认构造函数
编译器在类没有任何构造函数的情况下才会生成默认构造函数
一旦定义了其他的构造函数,那么除非再定义一个默认的构造函数,否则类将没有构造函数
依据:如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制
只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数
对于某些类,合成的默认构造函数可能执行错误的操作。如:定义在块内的内置类型或复合类型(eg:数组或指针)的对象被默认初始化后,它们的值将是未定义的
含有内置类型或复合类型成员的类应该在类内初始化这些成员,或定义一个自己的默认构造函数。否则可能在创建对象时得到一个未定义的值
如果含有内置类型或复合类型成员,只有当这些成员全都被赋予了类内的初始值时,此类才适合使用合成的默认构造函数
有时候编译器不能为某些类合成默认构造函数,eg:类包含一个其他类类型的成员却该成员的类型没有默认构造函数,则编译器无法初始化该成员.
此时,就必须自定义默认构造函数.
定义Sales_data的构造函数
=default的含义
Sales_data()=default;
该构造函数不接受任何实参,故它是一个默认构造函数。作用完全等同于合成默认构造函数
C++11中,若需要默认的行为,则可通过在参数列表后加=default
要求编译器生成构造函数。
=default
既可和声明一起在类的内部,也可以作为定义出现在类的外部
在类内部则默认构造函数是内联的;若在类外部,则该成员默认情况下不是内联的
上方的默认构造函数对Sales_data有效,是因为已为内置类型的数据成员提供了初始值。
若编译器不支持类内初始值,那么你的默认构造函数就应使用构造函数初始值列表来初始化类的每个成员
构造函数初始值列表
Sales_data(const std::string& s) :bookNo(s) {}//表示ISBN编号;编译器赋予其他成员默认值 Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) {}//ISBN编号、售出的图书数量、图书的售出价格
参数列表和花括号之间的部分就称为构造函数初始值列表
负责为新建的对象的一些数据成员赋初值。
每个成员名字后紧跟括号括起来的(或花括号内)成员初始值
当初始值列表没有对某些数据成员进行显式的初始化时,它将以与合成默认构造函数相同的方式进行隐式初始化
构造函数不应随意覆盖类内的初始值,除非新赋值和原值不同
如果不能使用类内初始值,则所有构造函数都应显式的初始化每个内置类型的成员
在类的外部定义构造函数
Sales_data::Sales_data(std::istream& is)//此构造函数没有构造函数初始值列表,即其构造函数初始值列表为空 { read(is, *this);//此处的第二个参数是要给Sales_data对象的引用 }
由于执行了构造函数体,故对象的成员仍能被初始化
拷贝、赋值和析构
若不主动定义,则编译器将自动合成这些操作。
一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁。
某些类不能依赖于合成的版本
特别是,当类需要分配类对象之外的资源时,合成的版本常会失效。例如管理动态内存的类通常不能依赖于上述操作的合成版本
很多需要动态内存的类能用vector或string管理必要的存储空间。
使用vector和string能避免分配和释放内存带来的复杂性
类包含vector或string成员时,其拷贝、赋值和销毁的合成版本可以正常工作
在学习Chapter 13 拷贝控制如何自定义操作前,类中所有分配的资源都应直接以类的数据成员的形式存储
访问控制与封装
public:
- 成员在整个程序内可被访问,public成员定义类的接口
private:
- private成员可被类的成员函数访问,但不能被使用该类的代码访问,private部分封装了(隐藏了)类的实现细节
class Sales_data { public://添加了访问说明符 Sales_data() = default;//既然已经定义了其他构造函数,那也必须定义一个默认构造函数 Sales_data(const std::string& s) :bookNo(s) {}//表示ISBN编号;编译器赋予其他成员默认值 Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) {}//ISBN编号、售出的图书数量、图书的售出价格 Sales_data(std::istream&); //从中读取一条交易信息 std::string isbn() const { return bookNo; }//常量成员函数 Sales_data& combine(const Sales_data&); private: //添加了访问说明符 double avg_price() const { return units_sold ? revenue / units_sold : 0; }; std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; };
一个类可包含0个或多个访问说明符,且对于某个访问说明符能出现多次也没有严格限定。
访问说明符指定接下来的成员访问级别,有效范围直到出现下一个访问说明符或类的结尾处
使用class或struct关键字
struct和class的区别:唯一的一点区别:默认访问权限
类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式
- 若使用struct,则定义在第一个访问说明符之前的成员时public
- 若使用class,则默认访问权限是private
友元
类可允许其他类或者函数访问它的非公有成员,方法:令其他类或函数称为其友元
语法:
friend function(parameter-list); friend class className;
将Sales_data的非成员函数变为友元函数
friend Sales_data add(const Sales_data&, const Sales_data&); friend std::ostream& print(std::ostream&, const Sales_data&); friend std::istream& read(std::istream&, Sales_data&);
友元声明只能在类内部,但不限类内的具体位置。
友元不是类的成员,不受其所在区域的访问控制级别约束
友元的声明
友元的声明仅仅指定了访问的权限。
如希望能够调用友元函数,必须在友元声明之外再专门对函数进行一次声明。
把友元的声明与类本身放置在同一个头文件中(类外部)
类的其他特性
类成员再探
令成员作为内联函数
可以在类的内部把inline作为声明的一部分显示地声明成员函数,也可在类外部用inline关键字修饰函数的定义
inline Screen& Screen::move(pos r, pos c) { pos row = r * width; cursor = row + c;//在行内将光标移动到指定的列 return *this; }
重载成员函数
成员函数可被重载
可变数据成员
mutable 关键字
通过在变量中声明mutable可使数据成员即使在const成员函数内都能被修改
//可变数据成员 mutable关键字 //使某个数据成员即使在const成员函数内也可被修改 mutable size_t access_ctr;//即使在一个const对象内也能被修改
即使在一个const对象内也能被修改
类数据成员的初始值
默认情况下,希望类开始时总是拥有一个默认初始化的类。
在C++11中,最好的方式:把这默认值声明成一个类内初始值
class Window_mgr { private: std::vector<Screen> screens{ Screen(24,80,' ') };//默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen };
当提供一个类内初始值时,必须以 = 或花括号表示
返回* this的成员函数
返回引用的函数是左值的,这些函数返回的是对象本身而非副本
从const成员函数返回* this
因为display是一个const成员,此时this将是一个指向const的指针而* this是const对象。
以此判断display的返回类型是const Sales_data&
若令display返回一个const的引用,将不能把display嵌入一组动作的序列中
Screen myScreen; myScreen.display(cout).set('*');//若display返回常量引用,则调用set将引发错误
display的const版本返回的是常量引用,而无权set一个常量对象
一个const成员函数如果以引用的形式返回*this,那它的返回类型将是常量引用
基于const的重载
通过七分成员函数是否是const的,可以对其进行重载,原因与之前根据指针参数是否指向const而重载函数的原因差不多
- 非常量版本的函数对于常量对象是不可用的,故只能在一个常量对象上调用const成员函数。
- 可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是更好的匹配
public: ...... ...... ...... //根据对象是否是const重载了display函数 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; }
当display的非常量版本调用do_display时,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针
在非常量版本中,this指向一个非常量对象,因此display返回一个普通的(非常量)引用,const成员返回一个常量引用
Screen myScreen1(5, 3); const Screen blank(5, 3); myScreen1.set('#').display(cout);//调用非常量版本 blank.display(cout);//调用常量版本
因为do_display时在类内部定义的,所以它被隐式地声明成内联函数,故调用它不会带来任何额外地运行时开销
类类型
对于两个类来说,即使它们地成员完全一样,它们也是两个不同的类型
struct First{ int memi; int getMem(); }; struct Second{ int memi; int getMem(); }; First obj1; Second obj2=obj1;//err obj1与obj2的类型不同
Note:
即使两个类的成员 列表完全一致,它们也是不同的类型。
对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事
Sales_data item1; class Sales_data item1;//等价
类的声明
类的声明和定义可以分离开来
向前声明:仅仅声明类而暂时不定义
class Screen;
对于类型Screen来说,在它声明之后定义之前是一个不完全类型 即知道其是一个类类型,但不知道其包含什么成员
不完全类型使用前景:
- 可以定义指向这种类型的指针或引用
- 可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数
对于一个类来说,在创建它的对象前该类必须被定义过,而不能只是被声明。
类必须先被定义才能用引用或指针访问其成员
在类的静态成员中有一种例外的情况:
知道类被定义之后数据成员才能被声明成这种类类型。 换句话说:必须先完成类的定义,编译器才知道存储该数据成员需要多少空间。 因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能时该类自己 然而,一旦一个类的名字出现后,就被认为已经声明过了(但未定义) 因此类允许包含指向它自身类型的引用或指针
class Link_screen { Screen window; Link_screen *next; Link_screen *prev; }
友元再探
可以将非成员函数定义成友元,还可以把其他类定义成友元,也可以把其他类(已经定义)的成员函数定义成友元
友元函数能定义在类的内部,且为隐式内联
类之间的友元关系
一个类A要访问另一个类B的私有成员时,被访问的类A需要将B指定成它的友元
class Screen { friend class Window_mgr;//Window_mgr的成员可以访问Screen的私有部分 }
友元关系不存在传递性。Window_mgr的友元不具有访问Screen的特权
Note:
每个类负责控制自己的友元类或友元函数
令成员函数作为友元
friend void Window_mgr::clear(ScreenIndex);//clear必须在Screen类之前被声明
函数重载和友元
如果一个类想把一组重载函数声明成友元,需要对这组函数中的每一个分别声明
extern std::ostream& storeOn(std::ostream&, Screen&); extern BitMap& storeOn(BitMap&, Screen&); class Screen{ friend std::ostream& storeOn(std::ostream & ,Screen &); }
友元声明和作用域
类和非成员函数的声明并不必须在它们的友元声明之前。
当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的
但,友元本身不一定真的声明在当前作用域 (参见 友元的声明)
就算再类的内部定义该函数,也必须在类的外部提供相应的声明从而使得函数可见,
换言之,即使仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:
struct X{ friend void f(){/*友元函数可以定义在类的内部*/} X(){f();}//error f还没被声明 void g(); void h();` }; void X::g(){return f();}//error f还没被声明 void f(); void X::h(){return f();}//f的声明在作用域中了
友元声明的作用是影响访问权限,其本身并非普通意义上的声明。
Note:
有的编译器并不强制执行上述关于友元的限定规则
类的作用域
每个类都会定义自己的作用域。
在作用域外,普通的数据和函数成员只能由对象、引用或指针使用成员访问运算符来访问
对于类类型成员则使用作用域运算符访问。跟在运算符之后的名字都必须是对应类的成员
Screen::pos ht = 24, wd = 80; Screen scr(ht, wd, ' '); Screen* p = &scr; char c = scr.get();//访问scr对象的get成员 c = p->get();//访问p所指对象的get成员
在类的外部,成员的名字被隐藏起来了
一旦遇到类名,定义的剩余部分(参数列表和函数体)就在类的作用域之内。结果就是,可以直接使用类的其他成员而无须再次授权