开始目录
引言
- C++在人们的眼中通常是“复杂”一词的代表,但其实C++有严谨的体系结构,以C++构建起来的工程不仅严谨并且执行速度非常快。
- 计算机科学本就是一门工程学科,C++就相当于是一种搭建房子的基础方法,学完C++我们将得到一把强而有力武器来武装我们自己。
- 如果你想做游戏引擎开发,或者从事游戏相关行业,那么C++会是你最好的武器。当我在CSDN上不断查找C++相关知识,调试几十次程序依旧报错时,我知道我应该正视C++并且认真系统的去学习它了。
- 本系列文章是我系统性学习C++的读书笔记,我具有C语言和Java编程基础。如果你没有编程基础,那么本文将引导你去思考:“我应该学会什么”。如果你和我一样,那么这会是一趟很美好的旅程!
一、开发环境和参考书籍
- 本系列文章均使用Visual Studio2019编写、调试、运行程序,关于C++开发环境的配置不做阐述。
- 本系列文章内容均参考数据《C++ Primer》第五版。
二、一个简单的C++程序
- 本文将提及许多概念性的内容,比如函数是怎样定义的。如果你具有很丰富的编程经验,我想看一看也是非常好的,这将使你在一个全新的高度审视一门编程语言的本质。
- 一个简单的C++程序如下:
int main()
{
return 0;
}
- 每个C++程序都包含一个或多个函数,其中一个必须命名为main,操作系统通过调用main来运行C++程序。
- 一个函数的定义包含四部分:返回类型、函数名、形参列表、函数体,函数的定义格式如下:
返回值类型 函数名(参数列表)
{
函数体
}
- 参数列表用括号"()“包围,函数体使用花括号”{}“包围,其实 " { 函数体 } " 定义了一个语句块。C++中的语句以”;"结尾。
- main函数的返回类型必须为int,即整数类型,这是一种内置类型,即C++语言自身定义的类型。
- 本例main函数函数体里包含的语句只有一句,即 " return 0; " ,它的作用是结束函数的执行。由于main函数被定义为需要返回一个int类型的值,因此return语句即结束了函数的执行也需要返回一个值。函数将向调用者返回值,在本例中main函数返回了0,由于它的调用者是操作系统,因此它将返回给操作系统" 0 "。
- 在大多数系统中,main函数的返回值被用来指示程运行的状态,返回0表示成功,非0返回值的含义由系统定义,通常用来指出错误的类型。
- 数据的类型是一个重要的概念,它不仅定义了数据元素的内容,还定义了数据上可以进行的操作。
- 程序所处理的数据都保存在变量中,每个变量都有自己的类型。
- 改写程序,将main函数体中return语句的返回值修改为-1,程序运行后会得到如下结果:
三、初识输入和输出
- C++语言并未定义任何输入输出(IO)语句,而是包含了一个全面的标准库(standard library)来提供ID机制(以及很多其他设施)。
- iostream库包含两个基础类型:istream和ostream。它们分别表示输入流和输出流。一个"流"就是一个字符序列,是从IO设备读出或写入IO设备的。术语"流"想要表达的是:随着时间的推移,字符是顺序生成或消耗的。
标准输入输出对象
- 标准库定义了四个IO对象。为了处理输入,我们使用一个名为cin的istream类型对象,这个对象也被称为标准输入(standard input)。对于输出,我们使用一个名为cout的ostream类型对象,此对象也被称为标准输出(standard output)。标准库中还定义了其他两个ostream对象,名为cerr和clog,我们通常用cerr来输出警告和错误信息,因此它也被称为标准错误(standard error),而clog用来输出程序运行时的一般性消息。
- 系统通常将程序所运行的窗口和上述输入输出对象关联起来。因此当我们读取cin时,数据将从程序正在运行的窗口读入。当我们向cout、cerr和clog写入数据时,将会写到同一个窗口。
- 程序员使用cin输入消息、cout输出消息,但在操作系统看来,它应该使用cin读取用户的输入,使用cout写入到窗口上展示给用户。
- 设计一个程序,使用标准输出提示用户输入,使用标准输入读取用户输入:
#include <iostream>
int main()
{
std::cout << "请输入两个整数:";
int n1, n2;
std::cin >> n1 >> n2;
std::cout << "它们的和为:" << n1 + n2;
return 0;
}
- 运行结果:
- 语句解析:
#include <iostream>
- 这一行代码告诉编译器我们想要使用iostream库。语句尖括号中的名字指出了头文件(hander),"#include"指令和头文件的名字必须写在同一行中。通常情况下,#include指令必须出现在所有函数之外,我们一般将一个程序的所有#include指令都放在源文件的开始位置。
std::cout << "请输入两个整数:";
std::cin >> n1 >> n2;
std::cout << "它们的和为:" << n1 + n2;
- 语句"std::cout << “请输入两个整数:” “是一个表达式。在C++中一个表达式产生一个计算结果,它由一个或多个运算对象和(通常是)一个运算符组成。这条语句中的表达式使用了输出运算符”<<"在标准输出上打印消息。
- "<<“运算符接受两个运算对象:左侧的运算对象必须是一个ostream对象,右侧对象是要打印的值,此运算符将给定的值写到给定的ostream对象中。输出运算符”<<"的计算结果就是其左侧运算对象,即计算结果就是我们写入给定值的那个ostream对象。
- 语句"std::cout << “它们的和为:” << n1 + n2;“使用了两次输出运算符”<<",因为此运算符返回其左侧的运算对象,因此第一个运算符的结果成为了第二个运算符的左侧运算对象,这样我们就可以将输出对象连接起来。这条语句等价于:
(std::cout << "它们的和为:") << n1 + n2;
- 当然我们也可以将两次运算分开:
std::cout << "它们的和为:";
std::cout << n1 + n2;
- "它们的和为"是一个字符串字面值常量,是用一对双引号包围的字符序列。而std::endl表示的endl是一个被称为操纵符的特殊值,将endl写入到标准输出对象的效果是:结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出真正的写入输出流中,而不是仅停留在内存中等待写入流。
- 程序员常常在调试时添加打印语句,这类语句应该保证”一直“刷新流,否则如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃为止的错误推断。
- "std::“前缀表示的是使用的cout和cin定义在名为std的命名空间中。标准库定义的所有名字都在命名空间std中。”::"符号为作用域运算符,作用是指出我们想使用的定义在哪个命名空间中。
- 输入运算符">>"与输出运算符类似,接受一个istream作为其左侧运算对象,接受另一个对象作为其右侧运算对象。语句"std::cin >> n1 >> n2;"从给定的istream对象中读入数据,并保存进给定的对象中,输入运算符<<同样返回其左侧运算符对象作为其计算结果,因此上述语句等价于:
std::cin >> n1;
std::cin >> n2;
- 有意思的是 “它们的和为:” 的类型是字符串字面值常量,而n1和n2的类型是int,但无论对于cin还是cout对象来说,它们之间够可以通过输入运算符>>和输出运算符<<进行运算。这是因为标准库定义了不同版本的输入输出运算符,来处理这些不同类型的运算对象。
四、注释
- C++中有两种注释:单行注释和多行注释:
#include <iostream>
int main()
{
// 单行注释
/*
多行注释
我也是注释
*/
return 0;
}
- 注释中的所有内容都将被编译器忽视。需要注意的是,注释界定符并不能嵌套,多行注释必须以 /* 开始,以 * / 结束。
五、控制流
- 程序一般是顺序执行的,程序设计语言提供了多种不同的控制流语句,允许我们写出更为复杂的执行路径。
while循环
- while循环伪代码如下,其中的"{}“表示代码块,如果循环体中只有一条语句,可以不写”{}"而直接在下一行写出循环语句。
while(判断条件)
{
循环体
}
for循环
- for循环和while循环一样使用花括号"{}“括起来的代码块来存储循环体(即循环语句集合),当只有一条循环语句时我们也可以取消代码块直接使用一条语句。for循环中 变量的定义 如"int i”,注意它只会在循环开始执行一次,以后循环都不会执行,并且定义的变量只在这个循环语句的代码块内有效(其作用域是循环的代码块)。
for(变量的定义;判断条件;变量的迭代)
{
循环体
}
- 所谓语句块(block)就是使用花括号包围的语句序列,语句块也是语句的一种,在任何要求使用语句的地方都可以使用语句块。
- 运算符:<= 、+= 、++。 <=运算符比较左右两个数的大小,返回一个布尔值表示真假。+=是一种复合赋值运算符,此运算符将右侧的对象加到左侧运算对象上,它本质上与一个加法结合一个赋值是相同的。++是前缀递增运算符,会将运算对象的值增加1。
六、数量不定数据的输入
- 如何读取数量不定的输入数据?实例代码如下:
#include <iostream>
int main()
{
int value, sum = 0;
while (std::cin >> value)
sum += value;
std::cout << sum;
return 0;
}
-
运行结果:
-
由于>>运算符返回的是左侧运算对象,因此循环检测的其实是std::cin。当我们使用一个istram对象作为条件时,其效果是检测流的状态,如果流是有效的,即流为遇到错误,那么检测成功。当遇到文件结束符(end-of-file)时,或遇到一个无效的输入(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
-
当我们读取文件时,文件末尾会自带文件结束符,而当我们在控制台窗口输入时,在window平台下按住ctrl+z即可打印出文件结束符(即图中的^z,它们是一个整体)。再按下回车,这时就会将控制台中输入的内容送到输入缓冲区中,std::cin就会读取一个个int值出来(因为它们是被" "字符即空格分离的),然后进行循环,最后读取到文件结束符cin状态变为无效循环停止。
-
我们说过当cin遇到无效类型时,也会变为无效状态,我们修改输入为" 1 2 3 4 a",运行结果如下:
-
编译器的一部分工作是寻找程序文本中的错误。编译器没有能力检查一个程序是否按照其作者的意图工作,但可以检查形式(form)上的错误。编译器最常见能检查出的错误有:语法错误、类型错误、声明错误。
七、C++ 缩进和格式
- C++程序很大程度上是格式自由的,就比如main函数可写为:
int main()
{
int value, sum = 0;
while (std::cin >> value)
sum += value;
std::cout << sum;
return 0;
}
- 我们要根据程序的可读性去考虑程序的格式风格,思考它会对程序的可读性和易理解性有什么影响,而一旦选择了一种风格,就要坚持使用。
八、类简介
- 我们通过定义一个类(class)来定义自己的数据结构,一个类定义了一个类型,以及与其相关联的一组操作。类类型(class type)。
- 为了使用类必须了解三件事情:
- (1)类的名称是声明
- (2)类是在哪里定义的
- (3)类支持什么操作
- 一般而言,类的作者决定了类类型对象上可以使用的所有操作。
- 当我们调试程序时,可能需要反复从键盘输入,这是非常枯燥的。大多数操作系统支持文件重定向,这种机制允许我们将标准输入和标准输出与命名文件关联起来。
使用一个类
- 类的代码如下(直接添加进VS STDIO项目即可,不用关心具体实现细节):
/*
* This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
* Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
* copyright and warranty notices given in that book:
*
* "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
*
*
* "The authors and publisher have taken care in the preparation of this book,
* but make no expressed or implied warranty of any kind and assume no
* responsibility for errors or omissions. No liability is assumed for
* incidental or consequential damages in connection with or arising out of the
* use of the information or programs contained herein."
*
* Permission is granted for this code to be used for educational purposes in
* association with the book, given proper citation if and when posted or
* reproduced.Any commercial use of this code requires the explicit written
* permission of the publisher, Addison-Wesley Professional, a division of
* Pearson Education, Inc. Send your request for permission, stating clearly
* what code you would like to use, and in what specific way, to the following
* address:
*
* Pearson Education, Inc.
* Rights and Permissions Department
* One Lake Street
* Upper Saddle River, NJ 07458
* Fax: (201) 236-3290
*/
/* This file defines the Sales_item class used in chapter 1.
* The code used in this file will be explained in
* Chapter 7 (Classes) and Chapter 14 (Overloaded Operators)
* Readers shouldn't try to understand the code in this file
* until they have read those chapters.
*/
#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined
#define SALESITEM_H
//#include "Version_test.h"
// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>
class Sales_item {
// these declarations are explained section 7.2.1, p. 270
// and in chapter 14, pages 557, 558, 561
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::ostream& operator<<(std::ostream&, const Sales_item&);
friend bool operator<(const Sales_item&, const Sales_item&);
friend bool
operator==(const Sales_item&, const Sales_item&);
public:
// constructors are explained in section 7.1.4, pages 262 - 265
// default constructor needed to initialize members of built-in type
#if defined(IN_CLASS_INITS) && defined(DEFAULT_FCNS)
Sales_item() = default;
#else
Sales_item(): units_sold(0), revenue(0.0) { }
#endif
Sales_item(const std::string &book):
bookNo(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is) { is >> *this; }
public:
// operations on Sales_item objects
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& operator+=(const Sales_item&);
// operations on Sales_item objects
std::string isbn() const { return bookNo; }
double avg_price() const;
// private members as before
private:
std::string bookNo; // implicitly initialized to the empty string
#ifdef IN_CLASS_INITS
unsigned units_sold = 0; // explicitly initialized
double revenue = 0.0;
#else
unsigned units_sold;
double revenue;
#endif
};
// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{ return lhs.isbn() == rhs.isbn(); }
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.isbn() == rhs.isbn();
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}
// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy (|lhs|) into a local object that we'll return
ret += rhs; // add in the contents of (|rhs|)
return ret; // return (|ret|) by value
}
std::istream&
operator>>(std::istream& in, Sales_item& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.revenue << " " << s.avg_price();
return out;
}
double Sales_item::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
#endif
- 主程序:
#include <iostream>
#include "H/Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2;
if (item1.isbn() == item2.isbn())
{
std::cout << item1 + item2;
}
return 0;
}
- item.isbn() 调用了Sales_item类对象item的成员函数,成员函数是定义为类的一部分的函数,有时也叫方法。其中".“符号表示点运算符,点运算符只能用于类类型的对象,即”."左侧必须是类类型对象,右侧是该类型对象的一个成员名,运算结果为右侧运算对象指定的成员。
- 当我们使用点运算符".“调用成员函数时,我们使用调用运算符”()"来调用一个函数。调用运算符是一对圆括号。
书店处理书籍信息程序
- 编写一个程序,包含类Sales_item。通过输入 唯一ID编号、销售数量、销售单价 来输入一个类对象的信息。一种书籍可能销售多次,即连续读入的书籍对象可能是一本书,如果它们的唯一ID相同,我们就把它们相加。最后输出所有书籍统计出的销售信息,格式为:ID号、销售总量、销售总额、平均单价。
#include <iostream>
#include "H/Sales_item.h"
int main()
{
// 定义第一本书籍类对象item1
Sales_item item1;
// 处理数据信息
if(std::cin >> item1)
{
// 定义第二本书籍类对象item2
Sales_item item2;
while (std::cin >> item2)
{
// 如果两本书的IBN编号一样(即是一本书)
if (item1.isbn() == item2.isbn())
// 合并记录
item1 += item2;
else
// 否则直接输出第一本书的信息
std::cout << item1 << std::endl;
}
// 当没有第二本书时直接输出第一本书的信息
std::cout << item1 << std::endl;
}
// 如果一本数据的信息都没有
else
{
// 提示错误
std::cerr << "No data!" << std::endl;
// 返回-1表示处理失败
return -1;
}
return 0;
}
- 运行结果:
- 如果没有书籍数据:
九、术语表
- 参数(实参):向函数传递的值
- 赋值,抹去一个对象的当前值,用一个新值取代
- 缓冲区:一个存储区域,用于保存数据。IO设施通常将输入或输出数据保存在一个缓冲区中,读写缓冲区的动作与程序中的动作是无关的。我们可以显式的刷新缓冲,以便强制将缓冲区中的数据写入输出设备。默认情况下,读cin会刷新cout缓冲;程序非正常终止也会刷新cout。
- 记住在调试输出的最后记得写endl,因为它会将与设备关联的缓冲区(buffer)中的内容刷新到设备中。缓冲刷新保证了到目前为止程序的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。
- 默认情况下写到cerr的数据是不缓存的,写到clog的数据是被缓冲的。
- 条件:0表示假,非零值表示真。
- 花括号:划定程序块的边界。
- 数据结构:数据及其上所允许的操作的一种逻辑组合。
- 编辑-编译-调试:使程序能正确执行的开发过程。
- 文件结束符:系统特定的标识,指出文件中无更多数据了。
- 头文件:使类或其他名字的定义可被多个程序使用的一种机制。程序通过 #include 指令使用头文件。
- 初始化:在一个对象创建的时候就给它赋值。
- 源文件:包含C++程序的文件。