C++预备
一个半循环
思考以下的循环方式和 while(! input.fail()) 的区别:
int rowNumber = 0; while(true) { int intValue; double doubleValue; input >> intValue >> doubleValue; if(input.fail()) break; cout << setw(COLUMN_WIDTH) << (rowNumber + 1) << " | "; cout << setw(COLUMN_WIDTH) << intValue << " | "; cout << setw(COLUMN_WIDTH) << doubleValue << endl; rowNumber++; }
这种循环优点在于错误发生时能及时停止循环,若是后者,则总是会多执行一次循环。使用一个半循环而不是简单的 while 循环是有益的。
事实上,以上的代码可以简化成这样:
int intValue; double doubleValue; while(input >> intValue >> doubleValue) { /* ... process values here ... */ }
直接将操作符运算放进 while 循环的判定条件中,能有效的减少代码的冗余。
c++程序是如何运行的?
c++程序的运行分为三个部分:
预处理(preprocessing) 编译(compile) 连接(link)
-
预处理:在预处理步骤中,称为预处理器的特殊程序会扫描 C++ 源代码并对其应用各种转换。例如,解析 #include 指令以使各种库可用,特殊标记(如 __FILE_ _和 _ _LINE__将替换为源文件中的文件和行号,#define-d 常量和宏替换为其适当的值
-
编译:在编译步骤中,编译器将 C++源文件读入、优化并转换为目标文件。这些目标文件是特定于机器的,但通常包含执行 C++ 文件中指定的指令的机器代码,以及一些额外的信息。在这个阶段,编译器将报告犯的任何语法错误,例如省略分号、引用未定义的变量或将错误类型的参数传递到函数中
-
连接:最后,在链接阶段,一个名为链接器的程序将构建最终可执行文件所需的所有目标文件收集在一起,将它们与特定于操作系统的信息捆绑在一起,最后生成一个多文件程序、抽象和可以运行和分发的预处理器可执行文件。在此阶段,链接器可能会报告一些最终错误,这些错误会阻止它生成有效的 C++ 程序。
包含保护(include guard)
运用以下代码来防止头文件被重复利用:
#ifndef file_Included #define file_Included #endif
头文件的后缀是 .h
#define 是什么?
运用以下格式,在整个代码中完成替换
#define phrase replacement
注意,被替换的判断是从第一个空格开始,#define 指令的替换部分由换行符前面的短语后的所有文本组成。同时默认替换后的文本将会是字符串。这个过程是在编译过程中进行的。如果全局定义了一个函数,那么在调用时会将其先转化成 c++代码。
inline, 内联函数
在计算机科学术语中,宏是内联的,因为编译器将“函数”的内容放在调用站点,而不是插入到函数代码的间接跳转。内联函数可能比非内联函数效率高得多.对一个函数使用 inline 关键字会建议编译器自动内联该函数,提高运行效率。inline 关键字只是对编译器的建议,如果编译器认为内联函数太难或成本太高,则可以忽略该关键字。但是,在编写短函数时,有时有助于将函数标记为内联以提高性能。
如以下代码
inline int Max(int one, int two) { return one > two ? one : two; }
向量小结
向量,作为一个允许我们使用单个变量存储元素序列的多功能工具,有着难以取代的非凡地位。向量可以储存所有类型的数据(不可以混储),没有元素数量限制,增减元素都十分方便。仅仅需要在头文件中引入 include<vector> 。比如完成一个简单的从大到小排序的程序:
#include<iostream> #include<string> #include<vector> #include<sstream> using namespace std; int GetInteger(){ int myInt; cin >> myInt; return myInt; } size_t GetSmallestIndex(vector<int>& v,size_t startIndex){ size_t bestIndex = startIndex; //size_t类型表示C中任何对象所能达到的最大长度,它是无符号整数。这里使用是因为index肯定是大于零的数 for (size_t i = startIndex; i < v.size(); ++i){ if (v[i] < v[bestIndex]) bestIndex = i; } return bestIndex; } void SelectionSort(vector<int> & v){ for (size_t i = 0; i < v.size(); ++i) { size_t smallestIndex = GetSmallestIndex(v, i); swap (v[i], v[smallestIndex]); } } int main(){ vector<int> values; //创建一个空向量 int kNumValues = 10; for(int i =0 ;i < kNumValues; ++i){ cout << "enter another value " << endl; int val =GetInteger(); values.push_back(val); //给向量赋值 } SelectionSort(values); for(size_t i = 0; i<values.size(); ++i){ cout << values[i]<< " "; } }
也能通过以下格式初始化向量
vector<double> myReals(20, 137.0); vector<string> myStrings(5, "(none)");
规定向量的元素上限以及填充内容。之后如果要改变向量元素上限可以使用`.resize() 以下是示例
vector<int> myVector; // Defaults to empty vector PrintVector(myVector); // Output: [nothing] myVector.resize(10); // Grow the vector, setting new elements to 0 PrintVector(myVector); // Output: 0 0 0 0 0 0 0 0 0 0 myVector.resize(5); // Shrink the vector PrintVector(myVector); // Output: 0 0 0 0 0 myVector.resize(7, 1); // Grow the vector, setting new elements to 1 PrintVector(myVector); // Output: 0 0 0 0 0 1 1 myVector.resize(1, 7); // The second parameter is effectively ignored. PrintVector(myVector); // Output: 0
部分常见的向量操作
Constructor: vector () | vector myVector; Constructs an empty vector |
---|---|
Constructor: vector (size_type size) | vector myVector(10); Constructs a vector of the specified size where all elements use their default values (for integral types, this is zero). |
Constructor: vector (size_type size, const T& default) | vector myVector(5, "blank"); Constructs a vector of the specified size where each element is equal to the specified default value. |
size_type size() const; | for(int i = 0; i < myVector.size(); ++i) { ... } Returns the number of elements in the vector |
bool empty() const; | while(! myVector.empty()) { ... } Returns whether the vector is empty. |
void clear(); | myVector.clear(); Erases all the elements in the vector and sets the size to zero. |
T& operator [] (size_type position); const T& operator [] (size_type position) const; T& at(size_type position); const T& at(size_type position) const; | myVector [0] = 100; int x = myVector [0]; myVector.at(0) = 100; int x = myVector.at(0); Returns a reference to the element at the specified position. The bracket notation [] does not do any bounds checking and has undefined behavior past the end of the data. The at member function will throw an exception if you try to access data beyond the end. We will cover exception handling in a later chapter |
void resize(size_type newSize); void resize(size_type newSize, T fill); | myVector.resize(10); myVector.resize(10, "default"); Resizes the vector so that it's guaranteed to be the specified size. In the second version, the vector elements are initialized to the value specified by the second parameter. Elements are added to and removed from the end of the vector, so you can't use resize to add elements to or remove elements from the start of the vector |
void push_back(); | myVector.push_back(100); Appends an element to the vector |
T& back(); const T& back() const; | myVector.back() = 5; int lastElem = myVector.back(); Returns a reference to the last element in the vector. |
续表:
T& front(); const T& front() const; | myVector.front() = 0; int firstElem = myVector.front(); Returns a reference to the first element in the vector. |
---|---|
void pop_back(); | myVector.pop_back(); Removes the last element from the vector. |
iterator begin(); const_iterator begin() const; | vector:: iterator itr = myVector.begin(); Returns an iterator that points to the first element in the vector |
iterator end(); const_iterator end() const; | while(itr != myVector.end()); Returns an iterator to the element after the last. The iterator returned by end does not point to an element in the vector. |
iterator insert(iterator position, const T& value); void insert(iterator start, size_type numCopies, const T& value); | myVector.insert(myVector.begin() + 4, "Hello"); myVector.insert(myVector.begin(), 2, "Yo!"); The first version inserts the specified value into the vector, and the second inserts numCopies copies of the value into the vector. Both calls invalidate all outstanding iterators for the vector. |
iterator erase(iterator position); iterator erase(iterator start, iterator end); | myVector.erase(myVector.begin()); myVector.erase(startItr, endItr); The first version erases the element at the position pointed to by position. The second version erases all elements in the range [startItr, endItr). Note that this does not erase the element pointed to by endItr. All iterators after the remove point are invalidated. If using this member function on a deque (see below), all iterators are invalidated. |
Deque
称作 double_end_queue。也就是双端队列和 vector 相较有许多类似的操作,比如 resize,push.back 等。但是与 vector 最大的不同是,在 STL 实现的储存空间是不连续的大片“page”,而 vector 的是连续的空间.
优点:(1) 随机访问方便,即支持 [ ] 操作符和 vector.at() (2) 在内部方便的进行插入和删除操作 (3) 可在两端进行 push、pop
缺点:占用内存多
四个新的容器类
引子
设想一个程序,它能计算出连续扔骰子,直到扔出的点数与第一次相同时所需的次数,以下是采用向量的实现方式:
#include<iostream> #include<vector> #include<random> #include<ctime> using namespace std; //摇骰子 int DieRoll(){ return (rand() %6) +1; //确保返回是一到六的整数 } int RunProcess(){ vector<int> generate; //储存重复前摇出的数 while(1){ int NextVaule = DieRoll(); for(size_t k =0 ; k < generate.size(); ++k){ if (generate[k] == NextVaule){ //满足说明已经重复了 return generate.size() + 1; //加一是为了返还摇动的总次数 } } generate.push_back(NextVaule); //不是就把数字保存进去 } } const size_t kNumIterations = 10000; int main(){ srand(static_cast<unsigned>(time(NULL))); float total2 = 0; for(size_t i = 0 ; i< kNumIterations ; ++i){ size_t total = 0; for(size_t k = 0; k< kNumIterations ;++k){ total += RunProcess(); } total2 += float(total) / kNumIterations; } cout << "ultimate unswer is " << total2 / kNumIterations <<endl; }
当我们扔常见的六面骰子时,结果似乎很合理:
当重复次数增加这个数字的精度也会变大。但是如果我们扔的是个八面骰子呢?
到重复时所需要的数字并没增大许多。那要是我们正在玩博德之门3,需要连续过两个高难检定呢?
仅仅需要大概6次就可以重复??虽然这是个概率论问题,但我们可以注意到我们所使用的容器——向量。答案在于 RunProcess 函数的实现。此函数的核心是一个 for 循环,用于检查向量中是否包含特定值。直观地说,向量保持元素的有序序列。对向量的主要操作通过在该序列中添加和删除元素、查找该序列中特定位置的元素等来维护该序列。对于此应用程序,我们希望存储一个无序的数字集合。我们不关心元素何时被添加到向量中或它们占据什么位置。相反,我们感兴趣的是向量中有哪些元素,特别是给定的元素是否在向量中。
set
当元素集合的内容比元素所在的容器内的实际序列更重要时,便可以采用set容器。跟向量相比,set提供了一个任意的,无序的特殊容器,并且对以下的操作有很好的支持:
-
将元素添加到集合中
-
从集合中删去元素
-
确认集合中是否包含特定元素
回到引子中那个问题,尝试用set来替换使用过向量的部分:
size_t RunProcess() { set<int> generated; while (true) { int nextValue = DieRoll(); /* Check if this value has been rolled before. */ if (generated.count(nextValue)) return generated.size() + 1; /* Otherwise, add this value to the set. */ generated.insert(nextValue); } }
运行后的结果还是大差不差。
set的基本操作
#include<set> #include<iostream> using namespace std; //容器之间的操作大多具有相似性 int main(){ set<int> myset; //插入元素 myset.insert(124); myset.insert(125); myset.insert(14); myset.insert(1324); myset.insert(1); //检查元素是否在集合中 if(myset.count(1)){cout<< "there is it" << endl;} if(!myset.count(1000)){cout << "there is not it" << endl;} //删除集合中元素 myset.erase(125); //也可以使用clear来清除元素,size来查看容器大小,等等类似操作 return 0; }
迭代器初探
为什么要使用迭代器?
在最开始的C语言学习中,我们就已经习惯了用for循环来遍历数组的操作。这当然是一个很简洁方便的方法,但假如我们要遍历的容器不是数组呢?甚至这个容器不存在自带的顺序呢?(就如刚才提到的set),这时候再使用for循环就显得力不从心了。但是对于用迭代器来说刚好。如下所示:
实际操作
#include<vector> #include<iostream> using namespace std; int main(){ vector<int> myVector = {9,564,94,71,62,6933,122,47,6}; for(vector<int>::iterator itr = myVector.begin(); itr != myVector.end();++itr){ cout << *itr << endl; } }
到这里还有一个问题,就是当遇见无序容器时,迭代器遍历的顺序是如何的?当使用向量或 deque 时,有一个自然的迭代顺序(从序列的开始到结束),但是当使用 STL 集时,排序的概念有点模糊。但是,集合的迭代顺序是明确指定的。通过迭代器遍历集合元素时,将按排序顺序访问元素,从最小的元素开始,到最大的元素结束。这在一定程度上解释了为什么 STL 集合只能存储使用 less-than 运算符可比较的元素:如果无法比较元素,则集合中没有明确定义的“最小”或“最大”元素。若要查看其实际操作,请考虑以下代码片段:
/* 生成随机数 */ set<int> randomNumbers; for (size_t k = 0; k < 10; ++k) randomNumbers.insert(rand()); /* 顺序打印集合内容 */ for (set<int>::iterator itr = randomNumbers.begin(); itr != randomNumbers.end(); ++itr) cout << *itr << endl;
小结
正如刚才所做到的,使用迭代器有三个主要操作:
-
取消迭代器的引用
-
将迭代器从一个位置推进到另外一个位置
-
比较两个迭代器是否相等
迭代器的功能当然不止读取内容,也能写入内容,如下:
for(vector<int>::iterator ite = myVector.begin(); ite != myVector.end(); ++ite) myVector.insert(123);
由于迭代器提供了一种间接读取和写入容器元素的方法,因此可以通过操作容器类中的迭代器来编写对任何容器类中的数据进行操作的函数。这些函数称为 STL 算法。
现在让我们尝试一下用迭代器对字符串类型的容器进行操作。假设我们想要打印某个字符串向量的所有元素的长度,我们很可能会这样写:
for (set<string>::iterator itr = mySet.begin(); itr != mySet.end(); ++itr) cout << *itr.length() << endl; // Error: Incorrect syntax!
原因是编译器把我们的代码理解成了\*(itr.length())
而不是(*itr).length()
。
为了解决此问题,所有 STL 迭代器都支持并调用了箭头运算符,该运算符允许您在当前正在迭代的元素上调用成员函数。例如,要打印出集合中所有字符串的长度<string>,正确的语法是:
for (set<string>::iterator itr = mySet.begin(); itr != mySet.end(); ++itr) cout << itr->length() << endl;
迭代的范围定义
容易注意到我们之前的循环控制是靠 mySet.begin()
与 mySet.end()
。前者指定迭代范围开始的第一俄国元素,后者定义迭代范围末尾的元素。单个迭代器指向容器类中的单个位置,并表示间接读取或写入该值的方法。一对迭代器定义两个位置,从而定义一系列元素。具体而言,给定两个迭代器 start 和 stop,这些迭代器定义从 start 开始到停止前一个位置结束的元素范围。使用数学符号,由开始和停止跨度定义的元素范围 [start, stop)。
由于集合本身把元素按顺序排列的性质,我们可以如下尝试一下:
set<int>::iterator stop = mySet.upper_bound(100); for(set<int>::iterator itr = mySet.lower_bound(10); itr != stop; ++itr) cout << *itr << endl;
upper_bound 和 lower_bound 的美妙之处在于,指定为函数参数的元素是否实际存在于集合中并不重要(假如要求的元素不存在就会直接返还到开头或者末尾)。例如,假设我们在包含 3 到 137 之间的所有奇数的集合上运行上述 for 循环。在本例中,集合中既不包含 10 也不包含 100。但是,代码仍将正常工作。lower_bound 函数向第一个元素返回一个迭代器,其大小至少与其参数一样大,并且在奇数集中将向元素 11 返回一个迭代器。类似地,upper_bound 向第一个元素返回一个迭代器,严格大于其参数,因此将向元素 101 返回一个迭代器。
集合的常用函数
Constructor: set<T>() | set<int> mySet; 构造一个空集合。 |
---|---|
Constructor: set<T>(const set<T>& other) | set<int> myOtherSet = mySet; 构造一个另一个集合的副本。 |
Constructor: set<T>(InputIterator start, InputIterator stop) | set<int> mySet(myVec.begin(), myVec.end()); 构造一个包含特定范围([start, stop))内元素的副本的集合。任何重复的元素都将被丢弃,并且元素将被排序。注意,这个函数接受来自任何来源的迭代器。 |
size_type size() const | int numEntries = mySet.size(); 返回集合中包含的元素数量 |
bool empty() const | if(mySet.empty()) { ... } 返回集合是否为空。 |
void clear() | mySet.clear(); 从集合中移除所有元素。 |
iterator begin() const_iterator begin() const | set<int>::iterator itr = mySet.begin(); 返回一个迭代器指向集合的开始。在就地修改元素时要小心。 |
iterator end() const_iterator end() const | while(itr != mySet.end()) { ... } 返还迭代器到最后一个元素后面 |
insert(const T& value) | 第一个版本将指定的值插入集合中。返回类型是一个包含指向元素的迭代器和一个bool值的对,bool值指示元素是否成功插入(真)或者是否已经存在(假)。 |
void insert(InputIterator begin, InputIterator end) | 第二个版本将指定范围的元素插入集合中,忽略重复项。 |
const_iterator find(const T& element) const | 如果指定元素存在,则返回指向该元素的迭代器;否则,返回end。 |
size_type count(const T& item) const | 如果指定元素包含在集合中,则返回1;否则,返回0。 |
size_type erase(const T& element) void erase(iterator itr); void erase(iterator start, iterator stop); | 从集合中移除一个元素。在第一个版本中,如果找到指定元素,则将其移除,并且如果元素被移除则函数返回1,如果元素不在集合中则返回0。第二个版本移除itr指向的元素。最后一个版本擦除[start, stop)范围内的元素。 |
iterator lower_bound(const T& value) | 返回指向第一个大于或等于指定值的元素的迭代器。这个函数对于获取一系列元素的迭代器特别有用,尤其是与upper_bound一起使用时。 |
iterator upper_bound(const T& value) | 返回指向第一个大于指定值的元素的迭代器。因为这个元素必须严格大于指定值,你可以迭代一个范围,直到迭代器等于upper_bound,以获得所有小于或等于参数的元素。 |