补充C++基础笔记。
类
定义抽象数据类型
- 定义在类内部的函数是隐式的inline函数。
- 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
- 定义类相关的非成员函数:如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
// 输入的交易信息包括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;
}
- 构造函数:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
1)合成的默认构造函数:如果存在类内的初始值,用它来初始化成员,否则默认初始化该成员;
2)某些类不能依赖于合成的默认构造函数:只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数;如果类包含内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合用于合成的默认构造函数。
3)构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型地成员。
4)构造函数:
// 构造函数初始化列表:
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
// 不使用类内初始值
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0) { }
- 拷贝、赋值和析构
访问控制与封装
- 访问控制符:public和private
class和struct定义类的唯一区别就是默认的访问权限,class默认private,struct默认public。 - 友元:类可以允许其他类或者函数访问它的非公有成员,方法式令其他类或者函数成为它的友元,如:
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&)
一般来说,最好在类定义开始或结束前的位置集中声明友元。
类的其他特性
- 类的声明:
不完全声明:class Screen;
引入了名字Screen并且指明Screen是一种类类型
声明之后定义之前是一个不完全类型
类的其他特性
- 类成员再探
可变数据成员:能够修改类的某个数据成员,即使再一个const成员函数内mutable size_t access_ctr
- 返回*this的成员函数:函数返回的是对象本身而非对象的副本
class Screen {
public:
Screen &set(char);
Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char c)
{
contents[cursor] = c; //设置当前光标所在位置的新值
return *this; //将this对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r * width + col] = ch; //设置给定位置的新值
return *this; //将this对象作为左值返回
}
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
- 基于const的重载:通过区分成员函数是否是const的,我们可以对其进行重载
class Screen {
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:
//该函数负责显示Screen的内容
void do_display(std::ostream &os) const {os << contents;}
};
Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); //调用非常量版本
blank.display(cout); //调用常量版本
- 类类型
每个类定义了唯一的类型:对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。 - 友元不具有传递性:每个类负责控制自己的友元类或友元函数。
构造函数
- 构造函数的初始值有时必不可少
一个小例子:
class ConstRef {
public:
ConstRef(int ii)
private:
int i;
const int ci;
int &ri;
}
下面的构造初始化将引发错误(成员ci和ri必须被初始化):
Const Ref::ConstRef(int ii)
{
i = ii; //正确
ci = ii; //错误:不能给const赋值
ri = i; //错误:ri没有初始化
}
构造函数的正确形式:
//显式地初始化引用和const成员
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
Note: 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。
- 成员初始化的顺序:最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。
- 小例子:提供cin作为接受istream&参数的构造函数的默认实参
Sales_data(std::istream &is = std::cin) { read(is, *this); }
- 委托构造函数:使用所属类的其他构造函数执行它自己的初始化过程
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) }
//其他成员函数与之前的版本一致
}
- 默认构造函数
默认初始化发生的情况:
(1) 在块作用区域不使用任何初始值定义一个非静态变量或者数组时;
(2) 当一个类本身含有类类型的成员且使用合成的默认构造函数时;
(3) 当类类型的成员没有在构造函数初始值列表中显式地初始化时。
值初始化发生的情况:
(1) 在数组初始化地过程中如果我们提供地初始值数量小于数组的大小时;
(2) 当我们不使用初始值定义一个局部静态变量时;
(3) 当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名。
如果想定义一个使用默认构造函数进行初始化地对象,正确的方法是在去掉对象名之后的空的括号对:
Sales_data obj(); //定义了一个函数而非对象
Sales_data obj; //obj是个默认初始化的对象
- 隐式的类类型转换:TBD
- 聚合类:TBD
- 字面值常量类:TBD
类的静态成员
- 声明静态成员:
小例子:表示银行账户记录的类
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();
}
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据,因此,每个Account对象将包含两个数据成员: owner和amount。只存在一个InterestRate对象且它被所有Account对象共享。
静态成员函数也不予任何对象绑定在一起,不包含this指针 -> 静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。
- 使用类的静态成员
作用域运算符直接访问:
double r;
r = Account::rate()
类的对象、引用或者指针来访问:
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate(); //通过Account的对象或引用
r = ac2->rate(); //通过指向Account对象的指针
成员函数不用通过作用域运算符就能直接使用静态成员。
- 定义静态成员:(和其他成员函数一样)既可以在类的内部,又可以在类的外部
void Account::rate(double newRate)
{
interestRate = newRate;
}
因为静态数据成员不属于类的任何一个对象,所以它们不是在创建类的对象是被定义的 -> 不由类的构造函数初始化
一般来说:不在类的内部初始化静态成员
Tip:要想保证对象只定义一次,最好的办法是把静态数据成员的额定义与其他非内联函数的定义放在同一个文件中。
- 静态成员的类内初始化:静态成员是字面值常量类型的constexpr
class Account {
public:
static double rate() { return interestRate; }
static void rate(double);
private:
static constexpr int period = 30; //period是常量表达式
double daily_tbl[period];
};
constexpr int Account::period; //初始值在类的定义内提供
静态成员可以用于的场景:
class Bar {
public:
//...
private:
static Bar mem1; //正确:静态成员可以是不完全类型
Bar *mem2; //正确:指针类型可以是不完全类型
Bar mem3; //错误:数据成员必须是完全类型
}
静态成员可以作为默认实参,非静态成员不能作为默认实参。因为它的值本身属于对象的一部分,这么做的结果是无法提供一个对象以便从中获取成员的值,从而引发错误。
class Screen {
public:
// bkground表示一个在类中稍后定义的静态成员
Screen& clear(char = bkground);
private:
static const char bkground;
}
参考资料:
- C++ primer. 第五版.