第一章 开始

我们的程序显然必须:

  • 定义变量
  • 进行输入和输出
  • 使用数据结构保存数据
  • 检测两条记录是否有相同的ISBN
  • 包含一个循环来处理销售档案中的每条记录

1.1 编写一个简单的C++程序

每个C++程序都包含一个或者多个函数,其中一个必须命名为main。操作系统通过调用main来运行C++程序。

下面是一个非常简单的main函数,它什么也不干,只是返回给操作系统一个值:

int main() {
    return 0;
}

一个函数的定义包含四部分

  • 返回类型
  • 函数名
  • 一个括号包围的形参列表
  • 函数体

main函数的返回类型必须为int,即整数类型。int类型是一种内置类型即语言自身定义的类型

函数定义的最后一个部分是函数体它是以包裹在花括号之内的语句块构成的

当return语句包括一个值时,此时返回值的类型必须与函数的返回类型相容。

大多数系统中,main函数的返回值被用来指示状态。返回值0表明成功,非0的返回值含义由系统定义,通常用来指出错误类型。

重要概念:类型

类型是程序设计最基本的概念之一,在本书中我们会反复遇到它。一种类型不仅定义了数据元素的内容,还定义了这类数据上可以进行的计算。

程序所处理的数据都保存在变量中,而每个变量都有自己的类型。如果一个名为v的变量的类型为T,我们通常说“v具有类型T”,或等价的,“v是一个T类型的变量”。

1.1.1 编译、运行程序

编写好程序之后,我们就需要编译它。如何编译程序依赖于你使用的操作系统和编译器。

程序源文件命名约定

程序文件通常被称为源文件。在大多数系统中,源文件的名字以一个后缀为结尾,后缀是由一个句点后接一个或多个字符组成的。不同的编译器使用不同的后缀命名约定。

从命令行运行编译器

运行GNU编译器的命令是g++:g++ -o prog1 prog1.cc

1.1节练习

练习 1.1:查阅你使用的编译器文档,确定它所使用的文件命名约定。编译并运行第2也得main程序。

int main() {
    return 0;
}

// 编译命令:g++ -std=c++11 main.cpp -o main
// 执行命令:./main

练习 1.2:改写程序,让它返回-1.返回值-1通常被当作错误的标识。重新编译并运行你的程序,观察你的系统如何处理main返回的错误标识。

int main() {
    return -1;
}
// g++编译器并没有出现任何的异常

1.2 初识输入输出

C++语言并未定义任何输入输出语句,取而代之,包含了一个全面的标准库来提供IO机制。

本书中很多示例都使用了iostream库。iostream库包含两个基础类型istream和ostream,分别表示输入流和输出流。一个流就是一个字符序列,是从IO设备读出或写入IO设备的。术语“流”想要表达的是,随着时间的推移,字符是顺序生成或者消耗的。

标准输入输出对象

标准库定义了4个IO对象。为了处理输入,我们使用一个名为cin的istream类型的对象。这个对象也被称为标准输入。对于输出,我们使用一个名为cout的ostream类型的对象。

当我们读取cin,数据将从程序正在运行的窗口读入,当我们像cout、cerr和clog写入数据时,将会写到同一个窗口。

一个使用IO库的程序

通过使用IO库,我们可以扩展main程序,使之能提事用户输入两个数,然后输出他们的和:

#include <iostream>

int main() {
    std::cout << "Enter two numbers: " << std::endl;
    int v1 = 0;
    int v2 = 0;
    std::cin >> v1 >> v2;
    std::cout << "The sum of " << v1 << " and " << v2
              << " is " << v1 + v2 << std::endl;
    return 0;
}

程序的第一行#include <iostream>告诉编译器想要使用iostream库。尖括号中的名字指出了一个头文件。每个使用标准库设施的程序都必须包含相关的头文件。#include指令和头文件的名字必须写在同一行中。通常情况下,#include指令必须出现在所有函数之外。我们一般将一个程序所有的#include指令都放在源文件卡是的位置。

向流写入数据

在C++中,一个表达式产生一个计算结果,它由一个或者多个运算对象和运算符组成。

输出运算符(>>)接受两个运算对象:左侧的运算对象必须是一个ostream对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream对象中。计算结果就是我们写入给定值的那个ostream对象。

第四行的语句我们可以等价的写成:

std::cout << "Enter two numbers:";
std::cout << std::endl;

字符串字面值常量:是一对用双引号包围的字符序列。

std::endl:操纵符,写入endl的效果是结束当前行并将与设备关联的缓冲区中的内容刷到设备中。缓冲区刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。

使用标准库中的名字

命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std中。

如果想使用一个命名空间中的名字,则必须使用namespace::name这样的形式,也就是作用域运算符::来指出特定的命名空间。

从流中读取数据

输入运算符(>>)与输出运算符类似,它接受一个istream对象作为其左侧运算对象,接受一个对象作为其右侧运算对象。他从给定的istream中读入数据,并存入给定的对象中。

我们可以将语句等价的写成:

std::cin >> v1;
std::cin >> v2;
完成程序

剩下的就是计算结果了。

1.2节练习

练习 1.3:编写程序,在标准输出上打印Hello, World。

#include <iostream>

int main() {
    std::cout << "Hello, World" << std::endl;
    return 0;
}

练习 1.4:我们的程序使用加法运算符+来将两个数相加。编写程序使用乘法运算符*,来打印两个数的积。

#include <iostream>

int main() {
    std::cout << "Enter two numbers: " << std::endl;
    int v1 = 0;
    int v2 = 0;
    std::cin >> v1 >> v2;
    std::cout << "The product of " << v1 << " and " << v2
              << " is " << v1 * v2 << std::endl;
    return 0;
}

练习 1.5:我们将所有输出操作放在一条很长的语句中。重写程序,将每个运算对象的打印操作放在一条独立的语句中。

#include <iostream>

int main() {
    std::cout << "Enter two numbers: " << std::endl;
    int v1 = 0;
    int v2 = 0;
    std::cin >> v1 >> v2;
    std::cout << "The sum of ";
    std::cout << v1;
    std::cout << " and ";
    std::cout << v2;
    std::cout << " is ";
    std::cout << v1 + v2;
    std::cout << std::endl;
    return 0;
}

练习 1.6:解释下面的程序片段是否合法。

std::cout << "The sum of" << v1;
          << " and " << v2;
          << " is " << v1 + v2 << std::endl;

不合法,因为在每行语句后面写上分号的话代表了一条独立的语句。应当修改为:

std::cout << "The sum of" << v1
          << " and " << v2
          << " is " << v1 + v2 << std::endl;

1.3 注释简介

错误的注释比完全没有注释更加糟糕。当修改代码时,不要忘记同时更新注释。

C++中注释的种类

C++中有两种注释:单行注释界定符对注释

单行注释:双斜线(//)开始,以换行符结束。当前行右侧所有的内容都会被编译器忽略。这种注释可以包含任何的文本,包括额外的双斜线。

界定符注释对:这种方式继承自C语言,以/开始,以/结束,可以包含除了*/之外的任何内容。包括换行符。

当注释界定符跨越多行时,最好能显式指出其内部的程序行都属于多行注释的一部分。我们所采用的风格是:注释内的每行都以一个星号开头,从而指出整个范围都是多行注释的一部分

注释界定符不能嵌套

一个注释不能嵌套在另一个注释之内。如果进行嵌套会产生很复杂的错误。

我们通常需要在调试期间注释掉一些代码。由于这些代码可能包含界定符形式的注释,所以我们使用单行注释的方法来进行操作。

1.3节练习

练习 1.7:编译一个包含不正确的嵌套注释的程序,观察编译器返回的错误信息。

// filename: a.cpp
int main() {
    /* */ */
    return 0;
}

// a.cpp: In function 'int main()':
// a.cpp:2:12: error: expected primary-expression before '/' token
//      /* */ */
//             ^
// a.cpp:3:5: error: expected primary-expression before 'return'
//      return 0;
//      ^~~~~~

练习 1.8:指出下列哪些输出语句是合法的(如果有的话):

std::cout << "/*"; // 合法

std::cout << "*/"; // 合法

std::cout << /* "*/" */; // 非法
// 应当修正为:std::cout << "*/";
    
std::cout << (/* "*/)" /* "(/*" */); // 合法

1.4 控制流

语句一般是顺序执行的:语句块的第一条语句首先执行,然后是第二条语句,依此类推。

1.4.1 while语句

while语句反复执行一段代码,直到给定的条件为假为止。

#include <iostream>

int main() {
    int sum = 0;
    int val = 1;
    // 只要val的值小于等于10,while循环就会持续执行
    while (val <= 10) {
        sum += val;
        ++val;
    }
    std::cout << "Sum of 1 to 10 inclusive is ";
              << sum << std::endl;
    return 0;
}

while语句的形式为:

while (condition) {
    statements;
}

while语句的执行过程是交替的检测condition条件和执行关联的语句statements,直至condition为假时停止。所谓条件就是产生真或者假的结果的表达式。只要condition为真,statements就会被执行。

条件中使用了小于等于运算符来比较val和10的大小关系。

所谓语句块,就是用花括号包围的零条或者多条语句的序列。语句块也是语句的一种,在任何要求使用语句的地方都可以使用语句块。

复合赋值运算符:将其右侧的运算对象加到左侧运算对象上,将结果保存到左侧的运算对象中。

前置递增运算符:将运算对象的值增加1。

1.4.1节练习

练习 1.9:编写程序,使用while循环将50到100的整数相加。

#include <iostream>

int main() {
    int sum = 0;
    int val = 50;
    while (val <= 100) {
        sum += val;
        ++val;
    }
    std::cout << "Result: " << sum << std::endl;
    return 0;
}

练习 1.10:除了++运算符将运算对象的值增加1之外,还有一个递减运算符--实现将值减少1。编写程序,使用递减运算符在循环中按递减顺序打印出10到0之间的整数。

#include <iostream>

int main() {
    int val = 10;
    while (val >= 0) {
        std::cout << val << " ";
        --val;
    }
    std::cout << std::endl;
    return 0;
}

练习 1.11:编写程序,提示用户输入两个整数,打印出这两个整数所指定的范围内的所有整数。

#include <iostream>

int main() {
    std::cout << "Enter two numbers: ";
    int begin = 0;
    int end = 0;
    std::cin >> begin >> end;
    while (begin < end) {
        std::cout << begin << " ";
        ++begin;
    }
    std::cout << std::endl;
    return 0;
}

1.4.2 for语句

每个for语句都包含了两部分,循环头和循环体。循环头控制循环体的执行次数,它由三部分组成:一个初始化语句、一个循环条件以及一个表达式。

定义在初始化语句中的变量只能在for循环之内使用,在循环结束之后是不能使用的。初始化语句只在for循环入口处执行一次。

循环体每次执行之前都会检查循环条件。如果条件为真然后就依次执行循环体和表达式,直到循环条件为假。

简要重述一下for循环的总体执行流程:

  1. 执行初始化语句,初始化语句中的变量只能在循环内使用。
  2. 检测循环条件是否为真,如果为真就依次执行循环体和表达式,否则退出循环。
  3. 重复2。
1.4.2节练习

练习 1.12:下面的for循环完成了什么功能?sum的终值是多少?

int sum = 0;
for (int i = -100; i <= 100; ++i) {
    sum += i;
}

这个for循环的功能是将-100到100的整数全部相加起来,最后的终值是0。

练习 1.13:使用for循环重做1.4.1节中的所有练习(第11页)。

// 练习 1.9:编写程序,使用while循环将50到100的整数相加。
#include <iostream>
int main() {
    int sum = 0;
    for (int val = 50; val <= 100; ++val) {
        sum += val;
    }
    std::cout << "Ans: " << sum << std::endl;
    return 0;
}

// 练习 1.10:除了++运算符将运算对象的值增加1之外,还有一个递减运算符--实现将值减少1。
// 编写程序,使用递减运算符在循环中按递减顺序打印出10到0之间的整数。
#include <iostream>
int main() {
    for (int val = 10; val >= 0; --val) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

// 练习 1.11:编写程序,提示用户输入两个整数,打印出这两个整数所指定的范围内的所有整数。
#include <iostream>
int main() {
    std::cout << "Enter two numbers: ";
    int begin = 0;
    int end = 0;
    for (std::cin >> begin >> end; begin <= end; ++begin) {
        std::cout << begin << " ";
    }
    std::cout << std::endl;
    return 0;
}

练习 1.14:对比for循环和while循环,两种形式的优缺点各是什么?

while循环一定会先检查循环条件。
for循环支持你写一些初始化的条件和断后表达式。
https://stackoverflow.com/questions/2950931/for-vs-while-in-c-programming

练习 1.15:编写程序,包含第14页“再探编译”中讨论的常见错误。熟悉编译器生成的错误信息。

No Present

1.4.3 读取数量不定的输入数据

我们可以将标准输入语句当作循环语句的条件来完成不定量的数据输入。

当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符,或者遇到一个无效的输入时,istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。

1.4.3节练习

练习 1.16:编写程序,从cin读取一组数,输出其和。

#include <iostream>

int main() {
    int sum = 0;
    int val = 0;
    while (std::cin >> val) {
        sum += val;
    }
    std::cout << "The sum is " << sum << std::endl;
    return 0;
}

1.4.4 if语句

if语句也对一个条件进行求值。如果条件为真,则执行程序块;否则不执行程序块或者执行else块中的语句。

1.4.4节练习

练习 1.17:如果输入的所有值都是相等的,本节的程序会输出什么?如果没有重复值,输出又会是怎样的?

如果所有的值都是相等的,程序只会打印一行
如果所有值都不一样,那么每个单词打印一行并标明一次

练习1.18:编译并运行本节的程序,给它输入全部相等的值。再次运行程序,输入没有重复的值。

#include <iostream>

int main() {
    int current_value = 0;
    int current_count = 0;
    int last_value = 0;
    if (std::cin >> current_value) {
        ++current_count;
        last_value = current_value;
        while (std::cin >> current_value) {
            if (current_value == last_value) {
                ++current_count;
            } else {
                std::cout << last_value << " occurs ";
                std::cout << current_count;
                std::cout << ((current_count > 1) ? " times" : " time");
                std::cout << std::endl;
                last_value = current_value;
                current_count = 1;
            }
        }
        std::cout << last_value << " occurs ";
        std::cout << current_count;
        std::cout << ((current_count > 1) ? " times" : " time");
        std::cout << std::endl;
    }
    return 0;
}

// 1 1 1 1 1 1^Z
// 1 occurs 6 times

// 1 2 3 4 5^Z
// 1 occurs 1 time
// 2 occurs 1 time
// 3 occurs 1 time
// 4 occurs 1 time
// 5 occurs 1 time

练习 1.19:修改你为1.4.1节练习1.10(第11页)所编写的程序(打印一个范围内的数),使其能够处理用户输入的第一个数比第二个数小的情况。

#include <iostream>

int main() {
    int value_one = 0;
    int value_two = 0;
    std::cout << "Enter two numbers: ";
    std::cin >> value_one >> value_two;
    if (value_one < value_two) {
        while (value_one <= value_two) {
            std::cout << value_one << " ";
            ++value_one;
        }
    } else {
        while (value_two <= value_one) {
            std::cout << value_two << " ";
            ++value_two;
        }
    }
    std::cout << std::endl;
    return 0;
}
C++程序的缩进与格式

C++程序很大程度上是格式自由的,也就是说,我们在哪里放置花括号、缩进、注释以及换行符通常不会影响程序的语义。

其他的格式总是存在的。当你要选择一种风格时,就要坚持使用。

1.5 类简介

在C++中,我们通过定义一个类来定义自己的数据结构。一个类定义了一个类型,以及于其关联的一组操作。

习惯上,我们约定C++的头文件被定义成.hpp后缀的文件。

1.5.1 Sales_item类

每个类实际上都定义了一个新的类型,其类型名就是类名。因此,我们的Sales_item类定义了一个名为Sales_item的类型。与内置类型一样,我们可以定义类类型的变量:

Sales_item item;

这个表达式声明了一个Sales_item类型的对象。

关键概念:类定义了行为

类Sales_item的作者定义了类对象可以执行的所有动作。一般而言,类的作者决定了类类型对象上可以使用的所有操作。

下面的程序从标准输入中读入数据,存入一个Sales_item对象中,然后将Sales_item的内容写回到标准输出中:

#include <iostream>
#include "Sales_item.h"

int main() {
    Sales_item book;
    // 以下两个操作需要涉及运算符的重载
    std::cin >> book; // 这里需要自己自定义标准输入的输入方式
    std::cout << book << std::endl; // 这里也需要自定义标准输出的方式
    return 0;
}

包含来自标准库的头文件时需要使用尖括号包围,对于不属于标准库的文件,则需要使用双引号来包围。

自定义类的对象的四则运算同样需要在类中进行运算符的重载。

1.5.1节练习

练习 1.20:编写程序读取一组销售记录,将每条记录打印到标准输出上。

#include <iostream>
#include <list>
#include <Sales_item.h>

int main()
{
    Sales_item book;
    std::list<Sales_item> book_list;
    while (std::cin >> book) {
        book_list.push_back(book);
    }
    for (auto item : book_list) {
        std::cout << item << std::endl;
    }
    return 0;
}

练习 1.21:编写程序,读取两个ISBN的相同的Sales_item对象,输出它们的和。

#include <iostream>
#include "Sales_item.h"
#include <list>

int main()
{
    std::list<Sales_item> book_list;
    Sales_item book;
    std::cout << "Enter the first book:";
    std::cin >> book;
    book_list.push_back(book);
    std::cout << "Enter the second book:";
    std::cin >> book;
    book_list.push_back(book);
    if (book_list[0].isbn() == book_list[1].isbn()) {
        std::cout << book_list[0] + book_list[1] << std::endl;
    } else {
        std::cout << "Error: isbns are not same."
    }
    return 0;
}

练习 1.22:编写程序,读取多个具有相同ISBN的销售记录,输出所有记录的和。

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item book;
    Sales_item temp;
    std::cin >> book;
    while (std::cin >> temp) {
        if (temp.isbn() == book.isbn()) {
            book = book + temp;
        } else {
            break;
        }
    }
    std::cout << book << std::endl;
    return 0;
}

1.5.2 初识成员函数

如果想要将两个Sales_item对象相加的程序首先应该检查两个对象是否具有想通的ISBN。

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item item1;
    Sales_item item2;
    std::cin >> item1 >> item2;
    // 首先检查item1和item2是否表示相同的书
    if (item1.isbn() == item2.isbn()) {
        std::cout << item1 + item2 << std::endl;
        return 0;
    } else {
        std::cerr << "Data must refer to same ISBN" << std::endl;
        return -1; // 返回-1表示失败
    }
}
什么是成员函数

成员函数是定义为类的一部分的函数,有时也被称为方法。我们通常使用类对象来调用成员函数。使用点运算符( . )来表达我们需要通过一个对象来调用成员函数来获取这个对象的属性。点运算符只能用于类类型的对象。其左侧运算对象必须是一个类类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象的指定成员

当使用点运算符访问一个成员函数时,通常我们需要调用这个函数。我们使用调用运算符( () )来调用一个函数(也就是一个可以被调用的对象)。调用运算符是一对圆括号,里面放置实参(argument)列表(可能为空)。

1.5.2节练习

练习 1.23:编写程序,读取多条销售记录,并统计每个ISBN(每本书)有几条销售记录。

#include <iostream>
#include <list>
#include <string>
#include "Sales_item.h"

struct BookInfo
{
    BookInfo(std::string i, int a) : 
        isbn(i), amount(a) { }
    std::string isbn() const { return isbn; }
    std::string isbn;
    int amount;
};

int main()
{
    std::list<BookInfo> book_infos;
    Sales_item temp_book;
    while (std::cin >> temp_book) {
        bool is_found = false;
        for (auto item : book_infos) {
            if (temp_book.isbn() == item.isbn()) {
                item.amount += 1;
                is_found = true;
                break;
            }
        }
        if (!is_found) {
            book_infos.push_back(BookInfo(temp_book.isbn(), 1));
        }
    }
    for (auto item : book_infos) {
        std::cout << item.isbn() << " " << item.amount << std::endl;
    }
    return 0;
}

练习 1.24:输入表示多个ISBN的多条销售记录来测试上一个程序,每个ISBN的记录应该聚在一起。

// 以上的程序可以接受不同的输入方式

1.6 书店程序

树上的书店程序还没有练习1.23的复杂……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容