如果我们希望在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。
getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符位置,然后把所读的内容存入到那个string对象中去(注意不存换行符)
判断字符串是否为空
返回string对象的长度,类型是string::size_type类型
auto len = line.size()
string str("some string");
//每行输出str中的一个字符
for (auto c: str)
count <<c << endl;
string s("Hello World!");
//punct_cnt的类型和s.size的返回类型一样
decltype(s.size()) punct_cnt = 0;
// 统计s中标点符号的数量
for (auto c: s) //对于s中的每个字符
if (ispunct(c)) //如果该字符是标点符号
++punct_cnt;
cout << punct_cnt << "punctuation characters in" << s << endl;
#include
using std::vector;
以vector为例,提供的额外信息是vector内所存放对象的类型:
vector<int> ivec; //ivec保存int类型的对象
vector<Sales_item> Sales_vec; //保存Sales_item类型的对象
vector<vector<string>> file; //该向量的元素是vector对象
还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化对象
vector<int> ivec(10, -1); //10个int类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!"); //10个string类型的元素,每个都被初始化为“hi!”
vector<int> v2; //空vector对象
for (int i = 0; i!=100; ++i)
v2.push_back(i); // 依次把整数值放到v2尾端
//循环结束后v2有100个元素,值从0到99
// b表示v的第一个元素,e表示v尾元素的下一位置
auto b = v.begin(), e = v.end(); // b和e的类型相同
C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。
但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素
经典使用场景为二分搜索
// text必须是有序的
// beg 和 end表示我们搜索的范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且我们还没有找到sought时执行循环
while (mid != end && *mid != sought)
{
if (sought < *mid) // 我们要找的元素在前半部分吗?
end = mid;
else
beg = mid + 1;
mid = beg + (end - beg)/2; //新的中间点
}
数组是一种类似标准库类型vector,但数组的大小确定不变。不允许拷贝和赋值
C++新标准引入了两个名为begin和end的函数,这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。
int ia[] = {0, 1, 2, 3, 4, 5, 6, 8, 9}; // ia是一个含有10个整数的数组
int *beg = begin(ia); //指向ia首元素的指针
int *last = end(ia); // 指向arr尾元素的下一位置的指针
sizeof 运算符返回一条表达式或一个类型名字所占的字节数。
sizeof运算符的结果部分地依赖于其作用的类型:
C++11 新标准引入了一种更简单的for语句,这种语句可以遍历容器或其他序列的所有元素。范围for语句的语法形式是:
for (declaration: expression)
statement
expression表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者vector或string等类型的对象,这些类型的共同特点是拥有呢能返回迭代器的begin和end成员。
declaration定义一个变量,序列中的每个元素都得到能转换成成该变量的类型。确保类型相容最简单的办法是使用auto类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this //返回调用该函数的对象
}
定义非成员函数的方式与定义其他函数一样,通常把函数的声明和定义分离开来。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
sum.combine(rhs); //把rhs的数据成员加到sum当中
return sum;
}
每个类都分别定义了它的对象呗初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象呗创建,就会执行构造函数。
不同于其他成员函数,构造函数不能呗声明成const的。当我们创建类的一个const对象时,知道构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
Sales_data() = default;
首先明确一点:因为该构造函数不接受任何实参,所以它是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。
在C++新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=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::Sales_data(std::istream &is)
{
read(is, *this); // read函数的作用是从is中读取一条交易信息, 然后存入this对象中。
}
类可以允许其他类或者函数访问它的非共有成员,方法是令其他类或者函数称为它的友员。如果类想把一个函数作为它的友员,只需要增加一条以friend关键字开始的函数声明语句即可。一般来说,最好在类定义开始或结束前的位置集中声明友元
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];
}
如果成员是const或者是引用的话,必须将其初始化。类似的,当成员属于某种类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
如果成员时const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为成员提供初值。
成员的初始化顺序与它们在类定义中的出现顺序一致
class ConstRef{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii):i(ii), ci(ii), ri(i) { }
有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。
声明静态成员
我们通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以时public的或private的。静态数据成员的类型可以时常量、引用、指针、类类型等。
头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。
c.push_back(t) 在c的尾部创建一个值为t或由args创建的元素。返回void
c.push_front(t) 在c的头部创建一个值为t或由args创建的元素,返回void
c.insert(p,t) 在迭代器p指向的元素之前创建一个值为t或由args创建的元素
新标准引入了三个新成员——emplace_front, emplace和emplace_back, 这些操作构造而不是拷贝元素。这些操作分别对应push_front,insert和push_back,允许我们将元素放置在容器头部,一个指定位置之前或容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存Sales_data元素:
包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的应用。
if (!c.empty()){
c.front() = 42; // 将42赋予c中的第一个元素
auto &v = c.back(); // 获得指向最后一个元素的引用
v = 1024; // 改变c中的元素
auto v2 = c.back(); // v2不是一个引用,它是c.back()的一个拷贝
v2 = 0; //未改变c中的元素
}
如果我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常:
vector<string> svec; //空vector
cout << svec[0]; //运行错误:svec中没有元素
cout << svec.at(0); // 抛出一个out_of_range异常
list<int> ilist(10, 42); // 10个int: 每个的值都是42
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25, -1); // 将10个值为-1的元素添加到ilist的末尾
ilist。resize(5); // 从ilist末尾删除20个元素
理解capacity和size的区别非常重要。容器的size是指它已经保存的元素;而capcacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
关联容器支持高校的关键字查找和访问。两个主要的关联容器类型是map和set. map中的元素是一些关键字-值对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字:set支持高校的关键字查询操作——检查一个给定关键字是否在set中,例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词。
一个经典的使用关联数组的例子是单词计数程序:
// 统计每个单词在输入中出现的次数
map<string, size_t> word_count; // string 到size_t的空map
string word;
while (cin >> word)
++word_count[word]; //提取word的计数器并将其加1
for (const auto &w : word_count) // 对map中的每个元素
// 打印结果
cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time")<<endl;
上一个示例程序的一个合理扩展是:忽略常见单词,如“this”,“and”,"or"等。我们可以使用set保存想忽略的单词,只对不在集合中的单词统计出现次数:
// 统计输入中每个单词出现的次数
map<string, size_t> word_count; // string 到size_t的空map
set<string> exclude = {"The", "But", "And"};
string word;
while (cin >> word)
// 只统计不在exclude中的单词
if ( exclude.find(word) == exclude.end())
++word_count[word]; // 获取并递增word的计数器
map和set类型都支持begin和end操作。与往常一样,我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。
// 获得一个指向首元素的迭代器
auto map_it = word_count.cbegin();
// 比较当前迭代器和尾后迭代器
while (map_it != word_count.cend())
{
//解引用迭代器,打印关键字-值对
cout << map_it->first << "occurs" <<map_it->second <<"times"<<endl;
++map_it; //递增迭代器,移动到下一个元素
}
由于map和set包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响:
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8} // ive有8个元素
set<int> set2; // 空集合
set2.insert(ivec.cbegin(), ivec.cend()); // set2有4个元素
set2.insert({1,3,5,7,1,3,5,7});
// 向word_count插入word的四种方法
word_count.insert((word, 1));
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word,1));
word_count.insert(map<string, size_t>::value_type(word, 1))
与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。这两个版本的erase与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回void。
关联容器提供一个额外的erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素,返回实际删除的元素的数量。我们可以用此版本在打印结果之前从word_count中删除一个特定的单词。
set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7,8,9};
iset.find(1); //返回一个迭代器,指向key==1的元素
iset.find(11); // 返回一个迭代器,其值等于iset.end()
iset.count(1); //返回1
iset.count(11); //返回0
全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。
除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。
动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
为了更容易地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一对象;unique_ptr则是“独占”所指的对象。
shared_ptr<string> p1; // shared_ptr, 可以指向string
shared_ptr<list<int>> p2; // shared_ptr, 可以指向int的list
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4 指向一个值为“999999999”的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
我们可以认为每个shared_str都有一个关联的计数器,通常称其伪引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_shared<int>(42); //r指向的int只有一个引用者
r = q; //给r赋值,令它指向另一个地址
// 递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。为此,C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。
// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int
方括号中的大小必须是整型,但不必是常量。
也可以用一个表示数组类型的类别别名来分配一个数组,这样,new表达式中就不需要方括号了:
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; // 分配一个42个int的数组,p指向第一个int
delete p; // p必须指向一个动态分配的对象或为空
delete [] pa; // pa必须指向一个动态分配的数组或为空
当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。拷贝和移动构造函数定义了当用同类型的另一个对象时做什么。
拷贝构造函数的第一个参数必须是一个引用类型,原因我们稍后解释。虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引用。
合成拷贝构造函数
class Sales_data {
public:
// 其他成员和构造函数的定义,如前
// 与合成的拷贝构造函数等价的拷贝构造函数的声明
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
// 与Sales_data的合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), //使用string的拷贝构造函数
units_sold(orig.units_sold), // 拷贝orig.units_sold
revenue(orig.revenue) //拷贝orig.revenue
{ } // 空函数体
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生
class Foo {
public:
Foo& operator={const Foo&}; //赋值运算符
}
合成拷贝赋值运算符
// 等价于合成拷贝赋值运算符
Sales_data&
Sales_data::operator = (const Sales_data &rhs)
{
bookNo = rhs.bookNo; //调用string::operator=
units_sold = rhs.units_sold; //使用内置的int赋值
revenue = rhs.revenue; //使用内置的double赋值
return *this; //返回一个此对象的引用
}
为了支持移动操作,新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。
重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。
重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
ostream &operator<<(ostream &os, const Sales_data &item)
{
os<< item.isbn() << " " <<item.units_sold << " " << item.revenue << " " << item.avg_price();
return os;
}
除了名字之外,这个函数与之前的print函数完全一样。打印一个Sales_data对象意味着要分别打印它的三个数据成员以及通过计算得到的平均销售价格,每个元素以空格隔开。
istream &operator>>(istream &is, Sales_data &item)
{
double price; //不需要初始化,因为我们先将读入数据到price,之后才使用它
is >> item.bookNo >> item.units_sold >> price;
if (is) // 检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败:对象被赋予默认的状态
return is;
}
在执行输入运算符时可能会发生下列错误:
算术运算符通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,常常位于一个局部变量之内,操作完成后返回该局部变量的副本作为其结果。如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。
// 假设两个对象指向同一本书
Sales_data
operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
sum += rhs; // 将rhs加到sum中
return sum;
}
通常情况下,C++中的类通过定义相等运算符来检验两个对象是否相等。也就是说,它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等。依据这一思想,我们的Sales_data类的相等运算符不但应该比较bookNo, 还应该比较具体的销售数据:
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs)
}
通常情况下关系运算符应该:
为了与内置类型的赋值运算符保持一致(也与我们已经定义的拷贝赋值和移动赋值运算一致),这个新的赋值运算符将返回其左侧运算对象的引用:
StrVec &StrVec::operator=(initializer_list<string> il)
{
// alloc_n_copy 分配内存空间并从给定范围内拷贝元素
auto data = alloc_n_copy(il.begin(), il.end());
free(); // 销毁对象中的元素并释放内存空间
elements = data.first; // 更新数据成员使其指向新空间
first_free = cap = data.second;
return *this;
}
和拷贝赋值及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,再创建一片新空间。不同之处是,这个运算符无须检查对象向自身的赋值,这是因为它的形参initializer_list确保il与this所指的不是同一个对象。
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符,例如,对于我们传递给stable_sort作为其最后一个实参的lambda表达式来说:
// 根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排序
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) {return a.size() < b.size();});
其行为类似于下面这个类的一个未命名对象
class ShorterString {
public:
bool operator() (const string &s1, const string &s2) const {return s1.size() < s2.size();}
};
表示lambda及相应捕获行为的类
当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。
相反,通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。举例,下面的lambda的作用是找到第一个长度不小于给定值的string对象:
// 获得第一个指向满足条件元素的迭代器,该元素满足size() is >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){ return a.size() >= sz;});
function是一个模板,和我们使用过的其他模板一样,当创建一个具体的function类型时我们必须提供额外的信息,在此例中,所谓额外的信息是指该function类型能够表示的对象的调用形式。参考其他模板,我们在一对尖括号内指定类型:
function
在这里我们声明了一个function类型,它可以表示接受两个int,返回一个int的可调用对象。因此,我们可以用这个新声明的类型表示任意一种桌面计算器用到的类型;
function<int(int, int)> f1 = add; //函数指针
function<int(int, int)> f2 = divide(); // 函数对象类的对象
function<int(int, int)> f3 = [](int i, int j){return i*j}; //lambda
使用这个function类型我们可以重新定义map:
// 列举了可调用对象与二元运算符对应关系的表格
// 所有可调用对象都必须接受两个int,返回一个int
// 其中的元素可以是函数指针,函数对象或者lambda
map<string, function<int(int, int)>> binops;
//我们能把所有可调对象,包括函数指针,lambda或者函数对象在内,都添加到这个map中:
map<string, function<int(int, int)>> binops = {
{"+", add}, // 函数指针
{"-", std::minus<int>()}, //标准库函数对象
{"/", divide()}, //用户定义的函数对象
{"*", [](int i, int j) {return i*j;}} //未命名的lambda
{"%", mod} //命名了的lambda对象
};
面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。
继承和动态绑定对程序的编写有两方面的影响:一是我们可以更容易定义与其他类相似但不完全相同的新类;二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略掉他们的区别。
继承:
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类别直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
派生类必须通过类派生列表明确指出它是从哪个基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符:
class Bulk_quote: public Quote{ // Bulk_quote继承了Quote
public:
double net_price(std::size_t) const override;// const在后,优先调用const生成的类对象
};
因为Bulk_quote在它的派生列表中使用了public关键字,因此我们完全可以把Bulk_quote的对象当成Quote的对象使用。
首先完成Quote类的定义:
class Quote{
public:
Quote() = default;
Quote(const std::string &book, double sales_price):bookNo(book), price(sales_price) {}
std::string isbn() const { return bookNo;}
// 返回给定数量的书籍的销售总额
// 派生类负责改写并使用不同的折扣计算算法
virtual double net_price (std::size_t n) const { return n*price;}
virtual ~Quote() = default; //对析构函数进行动态绑定
private:
std::string bookNo; // 书籍的ISBN编号
protected:
double price = 0.0; // 代表普通状态下不打折的价格
};
成员函数与继承
在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
class Bulk_quote: public Quote { //Bulk_quote 继承自Quote
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
// 覆盖基类的函数版本以实现基于大量购买的折扣政策
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
final和override说明符
如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误:
含有纯虚函数的类是抽象基类
抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。
当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中。
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
在容器中放置(智能)指针而非对象
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型:
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// 调用Quote定义的版本;打印562.5,即在15*&50中扣除掉折扣金额
cout << basket.back()->net_price(15) <<endl;
一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。
**类模板(class template)**是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。如我们已经多次看到的,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
template <typename T> class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<T> il);
// Blob中的元素数目
size_type size() const { return data->size();}
bool empty() const { return data->empty();}
// 添加和删除元素
void push_back(const T &t) (data->push_back(t);)
// 移动版本
void push_back(T &&t) {data->push_back(std::move(t));}
void pop_back();
// 元素访问
T& back();
T& operator[] (size_type i);
private:
std::shared_ptr<std::vector<T>> data;
// 若data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
};
实例化类模板
当使用一个类模板时,我们必须提供额外信息。我们现在知道这些额外信息是显示模板实参列表,它们被绑定到模板参数。
Blob<int> ia;
BlobL<int> ia2 = {0,1,2,3,4};