基本概念
成员函数
//头文件Sales_data.h 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; double revenue; }; Sales_data add(const Sales_data&, const Sales_data&); std::ostream &print(std::ostream&, const Sales_data&); std::istream &read(std::istream&, Sales_data&);
在内部声明,通常在外部定义。
成员函数通过一个额外的隐式形参this获取对象的地址,可以等价的认为是如下形式
total.isbn(); //Sales_data::isbn(&total)
在成员函数的内部,可以直接使用对象的成员,对类成员的直接访问可以被看做this的饮式引用。
std::string isbn() const { return this -> bookNo; }
this是一个常量指针。
常量成员函数
紧随参数列表后的const关键字,作用是修改隐式this指针的类型。默认下,this的类型仅是顶层常量指针(本身是常量,指向非常量),它的初始化就会使得我们不能把this绑定到一个常量对象上。导致的结果就是,我们不能在一个常量对象上调用普通的成员函数。
前提条件是在isbn函数体内不会改变this所指对象的成员,因为此时this的声明已经是const Sales_data *const,这样也可以提高函数的灵活性,毕竟常量对象(的引用或指针)只能调用常量成员函数。
成员函数的定义
通常放在外部,与声明保持一致,使用作用域运算符。
返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data& rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
当定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符,内置的赋值运算符把它的左值运算当成佐值返回,此处返回Sales_data的引用是为了与其保持一致。
类相关的非成员函数
虽然实际上并不属于类本身,但是类接口的组成部分,一般都在同一头文件中声明。
std::istream &read(std::istream &is, Sales_data& itm) { double price = 0; is >> itm.bookNo >> itm.units_sold >> price; itm.revenue = price * itm.units_sold; return is; }
读写操作会改变流的内容,所以都是普通引用
print函数不负责换行,执行输出任务的函数应该尽量减少对格式的控制。
构造函数
初始化类对象的数据成员,在类的对象被创建的同时执行。
没有返回类型。
不能声明成const的
合成的默认构造函数:由编译器创建,若存在类内的初始值,用其初始化成员;否则默认初始化各成员(vs中不支持)
带构造函数的struct类
struct Sales_data{ Sales_data() = default; //vs2010不支持类内初始值,Sales_data() {} 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 this -> bookNo; } Sales_data& combine(const Sales_data&); double avg_price() const; std::string bookNo; unsigned units_sold; double revenue; };
对于不支持类内初始值的编译器,可以在构造函数中添加一个赋值,例如:
Sales_data(const std::string &s): bookNo(s), units_sold(0){}
也可以在类的外部定义构造函数,如:
Sales_data::Sales_data(std::istream &is) { read(is, *this); }
注意,在类的声明部分,Sales_data(std::istream &is)
没有结构体。
拷贝、赋值和析构
若不主动定义,编译器将会对每个对象成员执行拷贝、赋值和销毁操作。
不过某些类不能依赖于合成的版本,比如管理动态内存的类。
访问控制与封装
访问说明符
public,成员可以在整个程序内被访问
private,仅可以被类的成员函数访问。
新的Sales_data类
class Sales_data{ public: Sales_data() {} Sales_data(const std::string &s): bookNo(s), units_sold(0){} 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 this -> bookNo; } Sales_data& combine(const Sales_data&); private: double avg_price() const; std::string bookNo; unsigned units_sold; double revenue; };
class和struct
struct定义在第一个访问说明符之前的成员是public的
class定义在第一个访问说明符之前的成员是private的
友元
使其它类或者函数访问它的非公有成员
仅仅是指定了访问权限,并非函数声明,所以还要再对这些函数做一次声明
添加了友元函数的类
class 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&); public: Sales_data() {} Sales_data(const std::string &s): bookNo(s), units_sold(0){} 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 this -> bookNo; } Sales_data& combine(const Sales_data&); private: double avg_price() const; std::string bookNo; unsigned units_sold; double revenue; };
类的其他特性
类型成员
先定义后使用
typedef std::string::size_type pos;
内联函数
定义在类内部的成员函数默认为inline
可以在类内部显式声明为内敛函数,也可以在外部使用inline修饰函数的定义。
无须再声明和定义处同时说明inline
可变数据成员
mutable关键字
永远不会是const,即使是const对象的成员
返回*this的成员函数
返回的的调用成员函数对象的引用Screen&,而非拷贝,这样就可以把一系列的操作连在一条表达式中
myScreen.move(4,0).set('#');
否则若返回类型是Screen,则set只会修改临时副本,而不改变myScreen
基于const的重载
display为一个const成员函数,返回*this,此时this是一个指向const的指针,*this是const对象。
返回类型即为const Screen&
此时myScreen.display().set('#');中,调用set自然就会引发错误。
使用const重载的方式根据对象是否为const判断。
class Screen{ public: Screen &display() {do_display(); return *this;} const Screen &display() const {do_display(); return *this;} private: void do_display(){}; };
类的声明
前向声明:class Screen
在声明之后定义之前是一个不完全类型,使用场景有限:定义指针、引用,声明(不能定义)作为函数参数或返回类型。
允许包含指向它自身类型的引用或指针。
友元类
友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
class Screen{ friend class Window_mgr; };
友元不存在传递性。
还可以只为某个类的成员函数提供权限。
class Screen{ friend void Window_mgr::clear(ScreenIndex); };
这样只有Window_mgr的成员函数clear可以随意访问Screen
对重载函数而言,虽然名字相同,但仍然是不同的函数,所以要分别对每一个进行友元声明。
友元的声明并不代表函数的声明,即便是在类的内部使用,也必须要在类的外部提供相应的声明。(有的编译器并不强制要求,如vs)
类的作用域
一个类就是一个作用域,对于定义在类外部的成员,要使用类名和作用域运算符,此时,定义的剩余部分就在作用域之内了。
名字查找顺序:由内而外逐层查找。
编译过程:1.编译成员的声明;2.类全部可见后才编译函数体
对于普通类成员,编译器会逐层向外查找,不用担心冲突。
但对于类型名而言,如果外层已经将其定义成为了一种类型,则类内部不能再重新定义。编译器可能并不对此负责。(vs不支持内外层重名)
在内外层重名的情况下,如果一定要使用外层的,可以使用作用域运算法
::height
类的静态成员
关键字static
与类本身直接相关,而不是与类的各个对象保持关联,被所有对象共享。
利用作用域运算符直接访问,类的对象也可以直接访问。
可以在类的外部定义,但不能重复static关键字。
静态数据成员不属于类的任何一个对象,故不是在创建类的对象时被定义,即不是由构造函数初始化的。不能在类的内部定义和初始化静态成员,必须在外部。
int Sales_data::testSta = 9;
可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是constexpr,初始值必须是常量表达式。例如可以用一个初始化了的静态数据成员指定数组的维度
static const int dim = 10; //static constexpr int dim = 10;
double array[dim];
如果静态成员的应用场景仅限于类似于上述编译器可替换的情况,则一个初始化的const或者constexpr不需要分别定义;相反则必须要有定义语句。
静态数据成员可以是不完全对象。
class Bar{ private: static Bar mem1; //正确 Bar* mem2; //正确,指针也可以是不完全对象 Bar mem3; //错误,数据成员不可以 };
可以使用静态数据成员作为默认实参。
构造函数详解
初始化与赋值
有时初始值必不可少,比如常量和指针。
class ConstRef{ public: ConstRef(int num); private: int i; const int ci; int &ri; };
上述类必须提供构造函数,此时若采用赋值的方式显然会引发错误,需要使用初始化的方式。
//错误 Screen(int ii){ i = ii; ri = ii; } //正确 Screen(int ii): i(ii), ri(ii) {}
初始化的顺序
理论上是按照成员在类中定义的顺序,尽量避免用一个成员初始化另一个成员。
默认实参和构造函数
Sales_data(std::string s = ""): bookNo(s), units_sold(0){} Sales_data(std::string s, unsigned n ,double p): bookNo(s), units_sold(n), revenue(p * n){}
注意默认实参不要冲突,特别是跟默认构造函数
Sales_data() {}
委托构造函数
C++11新标准,使用它所属类的其他构造函数执行它自己的初始化过程。
Sales_data(std::string s, unsigned n ,double p): bookNo(s), units_sold(n), revenue(p * n){} Sales_data(): Sales_data("", 0, 0) {} Sales_data(std::istream &is): Sales_data() {read(is, *this); }
受委托的构造函数的初始值列表和函数体被依次执行。先执行受委托函数体的代码,在交还给委托者的函数体。
默认构造函数
默认初始化发生情况
(1)在块作用域中不适用任何初始值定义一个非静态变量
(2)类中含有类类型成员且使用合成的默认构造函数
(3)类类型的成员没有在构造函数初始值列表中显式初始化
值初始化发生情况
(1)数组初始化中初始值数量不够
(2)不使用初始值定义以局部静态变量
(3)使用类似于int(8)
class NoDefault{ public: NoDefault(const std::string&); //还有其它成员,但是构造函数只有这一个 }; class A{ NoDefault mem1; }; A a; //错误,情况(2),不能为A合成构造函数 class B{ B(){} //错误,mem2不能没有初始值 NoDefault mem2; };
使用默认构造函数
Sale_data obj1(); //错误,obj1是个函数 Sale_data obj2; //正确
如果定义了其它的构造函数,最好顺便也提供一个默认构造函数。
隐式的类类转换类型
通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
string null_book = "99-99"; Sales_data item("12", 5, 4.4); item.combine(null_book);
相当于编译器利用给定的null_book自动创建了一个临时的Sales_data
编译器只会自动执行一步类型转换
item.combine("99-99"); //错误
抑制构造函数定义的隐式转换
关键字explicit
explicit Sales_data(std::string s = ""): bookNo(s), units_sold(0){}
只能在类内声明构造函数时使用,在类外部定义时不应重复
发生隐式转换的另一种情况是使用拷贝形式的初始化
Sales_data item = null_book;
当使用explicit时只能使用直接初始化
Sales_data item(null_book);
可以显式的直接使用
item.combine(Sales_data(null_book)); item.combine(Sales_data(cin)); item.combine(static_cast<Sales_data>(cin));
标准库中的
string中,单参数的const char*不是explicit的
vector中接受容量参数的,是explicit的
聚合类
(1)所有成员都是Public的
(2)没有定义任何构造函数
(3)没有类内初始值
(4)没有基类,没有virtual函数
把初始化的任务交给了类的用户,而不是作者
字面值常量类
除了算术类型、引用和指针外,有些类也是字面值类型
聚合类或者满足下列条件:
(1)数据成员全为字面值类型
(2)至少含有一个constexpr构造函数
(3)若某个数据成员含有类内初始值,则内置类型成员的初始值也必须是一条常量表达式;若是某种类类型,则初始值必须使用自己的constexpr构造函数
(4)必须使用析构函数的默认定义
constexpr的构造函数可以声明成默认=default形式,或者是删除函数的形式。
否则,构造函数的要求是:既不能包含返回语句(构造函数),又要保证唯一可执行的语句就是返回函数(constexpr函数的要求),所以函数体一般是空的。
前置关键字constexpr
必须初始化所有数据成员,使用初始值、constexpr构造函数或者常量表达式
constexpr构造函数用于产生constexpr对象以及constexpr函数的参数或返回类型。
vs中只用include头文件
一般都把声明放在头文件中,编译器先编译了所有的变量和函数的声明,然后再去具体查看函数的定义,vs会自动在各个cpp文件中编译函数,若重复include cpp文件,编译器会当做重名错误来处理