OOP之类和对象
1. this指针的引入
每个成员函数都有一个额外的隐含的形参,这个参数就是this指针,它指向调用对象的地址。默认情况下,this的类型是指向类类型非常量版本的常量指针。可以表示成如下伪代码形式:
/* 假设现在有一个类Sales_data,以及其非常量Sales_data类型对象,则该隐式的this指针可以写成如下伪代码形式 */ Sales_data *const this = &total;
this指针一般用于解决重名问题和返回自身的值或者引用。例如:
struct A{ int a; void test(int a){ this->a = a; } };
test函数的形参a和类成员a成名,根据就近原则,直接使用a,调用的是形参a,那么如何使用被屏蔽的成员a呢,这里就是采用this指针。
2. const成员函数
紧随参数列表之后的const关键字作用为:修改隐式this指针所指向的对象的类型,如下:
/* 假设现在有一个类Sales_data,以及Sales_data类型对象,则在const成员函数中隐式的this指针可以写成如下伪代码形式 */ const Sales_data *const this = &total;
这里加const的含义是,这个函数不能修改本对象,其实就是函数体内不得对类的成员进行修改。const主要起到保护的作用。
注意以下几点:
a)非const对象可以调用const成员函数,也可以调用非const成员函数,但是const对象只能调用const成员函数。并且,非const对象优先调用非const成员函数。
b)const成员函数只可以返回本对象的常量引用,如下写法会报错:
Student &print(ostream &os) const { os << id_ << " " << name_ << " " << age_ << endl; return *this; }
报错提示:
clang下:error: binding of reference to type 'Student' to a value of type 'const Student' drops qualifiers
return *this;
g++下:error: invalid initialization of reference of type ‘Student&’ from e
return *this;
最后记住:构造函数不能为const。如果为const,怎么完成初始化工作?!
3. const成员函数和非const成员函数可以构成重载。
到此为止,构成函数重载的要素有:类的名称、函数名、函数形参表以及成员函数的const属性。事实上,函数签名就是由这几个部分构成。
在这里我们解释一个问题: 为什么C语言里面没有函数重载? 因为在编译器编译C程序时会维护一张符号表,C语言在记载函数的时候就是简单的记录函数的名字,所以函数名就是C函数的唯一标识。当我们试图定义两个名字相同的函数时,就发生了重定义。
C++是怎么做的呢? 很显然,对于普通函数,它的符号(唯一标识)是根据函数名和参数列表生成的,对于类的成员函数,还要加上类名和const属性,所以我们进行函数重载的时候,这些函数在符号表中的标识是不相同的。 C++正是通过这种机制实现了函数的重载。
注意:C++编译器生成函数符号的时候没有考虑返回值,这也是函数重载和返回值无关的原因。
4. 构造函数之构造函数初始值列表(constructor initialize list)
构造函数有一个特殊的地方,就是它可以包含一个构造函数初始化列表,如下:
Person(int id, const string &name, int age) :_id(id), _name(name), _age(age){ }
虽然以下形式,也完全可以达到目的:
Person(int id, const string &name, int age){ _id = id; _name = name; _age = age; }
但两者是不同的。第一种形式带构造函数初始值列表,执行的是真正的初始化工作;而第二种形式,进行的是赋值操作。
注意,即使构造函数没有构造函数初始值列表(更确切的说是构造函数初始值列表为空),那么类中的成员变量将会执行默认初始化。因此在以下情况我们必须使用构造函数默认初始化列表:
a)const内置类型变量以及没有显示定义默认构造函数的const类类型变量(可以参考该博文合成的默认构造函数定义为delete的一种情况)
b)引用类型成员
c)没有默认构造函数的类类型变量
其本质是因为,const内置类型变量和引用类型必须初始化;而对于类类型对象,可以通过默认构造函数进行默认初始化(非const类类型对象只要有默认构造函数就可以默认初始化,而const类类型对象必须有显示定义的默认构造函数才可以执行默认初始化)
5. 类成员初始化的顺序是它们在类中声明的顺序,而不是初始化列表中列出的顺序
考虑下面的类:
class X { int i; int j; public: X(int val) : j(val), i(j) { } };
我们的设想是这样的,用val初始化j,用j的值初始化i,然而这里初始化的次序是先i然后j。
记住:类成员初始化的顺序是它们在类中声明的顺序,而不是初始化列表中列出的顺序!
6. 析构函数
与构造函数一样,析构函数也是一种特殊的函数。构造函数在对象被创建时调用,析构函数则是在对象被销毁时被调用。构造函数与构造函数一样,同样没有返回值,并且析构函数没有任何参数。如下:
~Person(){
}
需要引起注意的是:
a)对于类类型对象foo的析构函数只是在它生命期的最后一刻的回调罢了,管不了foo自己所占的内存,就像自己没法给自己收尸一样。
b)对于堆上的类类型对象:free 干的事情是释放内存。delete 干的事情是调用析构函数,然后释放内存,注意是delete释放的内存空间,而不是析构函数释放的。对于栈上的类类型对象,退出作用域时会自动调用析构函数,然后释放内存。
总结:对于栈上的类类型对象其实和内置类型变量一样,退出作用域后都是由系统自动释放内存的。实际上无论是栈空间,还是堆空间,内置类型对象和类类型对象销毁时的区别,在于类对象会在销毁前调用析构函数。
7. static成员
不用于普通的数据成员,static 数据成员独立于该类的任何一个对象而存在,每个static数据成员是与类关联,并不与该类的对象相关联。
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参(因为static成员不属于任何一个对象),它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员(因为没有this指针)。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处即可。
小结:
a)static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。
b)因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能是const成员函数。因为,将成员函数声明为 const 就是承诺不会修改该函数所属的对象,而 static 成员不是任何对象的组成部分。
c)static 函数只能使用 static 成员,而不能直接调用普通成员(方法+数据成员),当然如果这样写,static void print(Test &t) 谁也挡不住其调用对象t的普通成员。
d)static 成员一般在类内声明,类外定义。注意,当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处即可。
8. 友元
1. 必须先定义包含成员函数的类,才能将这个类的成员函数设置为另外一个类的友元。
2. 不必预先声明类和非成员函数来将它们设为友元。
#include <iostream> #include <string> #include <vector> using namespace std; class Test { public: friend class Other; //声明某个类是Test的朋友 friend void bar(const Test &t); //声明某个函数是Test的朋友 private: int x_; int y_; }; class Other { public: void foo(Test &t) { t.x_ = 10; t.y_ = 20; } }; void bar(const Test &t) { cout << t.x_ << endl; } int main(int argc, const char *argv[]) { Test t; return 0; }
注意:友元关系是单向的,以上例子中Test并不是Other的朋友,因此Test不能访问Other的private成员。(tmd,这不就是在告诉我们,你的是我的,我的还是我的)。顺便黑一下C++:
C++ is a modern language where your parent can't touch your privates but your friends can.
多么痛的领悟。
STL之顺序容器
1. 顺序容器的初始化
顺序容器主要是vector和list,他们的初始化方式有以下五种:
1. 直接初始化一个空的容器
2. 用一个容器去初始化另一个容器
3. 指定容器的初始大小
4. 指定容器的初始大小和初始值
5. 用一对迭代器范围去初始化容器
第2种和第5种初始化方式的区别在于:第2种不仅要求容器类型相同,还要求容器元素类型完全一致,而第5种不要求容器相同,对于容器元素,要求能相互兼容即可。
指针可以当做迭代器,所以可以这样做:
#include <iostream> #include <string> #include <vector> using namespace std; int main(int argc, char **argv) {
const size_t MAX_SIZE = 3; string arr[MAX_SIZE] = { "hello", "world", "foobar" }; vector<string> vec(arr, arr + MAX_SIZE); return 0; }
注意,凡是传入迭代器作为指定范围的参数,可以使用指针代替。
2. 容器元素的类型约束
凡是放入vector中的元素,必须具备复制和赋值的能力,因为放入vector中的元素只是一份拷贝。下例会报错。
#include <iostream> #include <string> #include <vector> using namespace std; //Test不支持复制和赋值。所以不能放入vector class Test { public: Test() {} private: //设为私有,禁用了Test的复制和赋值能力 Test(const Test &); //用于复制(拷贝构造函数) void operator=(const Test &); //用于赋值(赋值运算符) }; int main(int argc, const char *argv[]) { vector<Test> vec; Test t; vec.push_back(t); return 0; }
3. 特殊的迭代器成员 begin和end
有四个特殊的迭代器:
c.begin() //指向容器C的第一个元素
C.end() //指向最后一个元素的下一个位置
C.rbegin() //返回一个逆序迭代器,指向容器c的最后一个元素
C.rend() //返回一个逆序迭代器,指向容器c的第一个元素的前面的位置
分别去顺序迭代和逆序迭代容器,例如:
#include <iostream> #include <string> #include <vector> #include <list> using namespace std; int main(int argc, char **argv) { vector<string> vec; vec.push_back("beijing"); vec.push_back("shanghai"); vec.push_back("guangzhou"); vec.push_back("shenzhen"); for (vector<string>::iterator iter = vec.begin(); iter != vec.end(); ++iter) { cout << *iter << endl; } for (vector<string>::reverse_iterator iter = vec.rbegin(); iter != vec.rend(); ++iter) { cout << *iter << endl; } return 0; } /* output: beijing shanghai guangzhou shenzhen shenzhen guangzhou shanghai beijing */
4. 顺序容器的插入操作
1. vector没有push_front(vectoe内部实现是数组)。list有push_front。
2. 针对List
a)可以使用insert(p, t) 在指定位置元素之前添加元素,其中p是迭代器,t时元素的值
b)Insert(p, n, t) 在迭代器p指向的位置之前插入n个元素,初始值为t
c)Insert(p, b, e) 在迭代器p指向的位置之前插入迭代器b和迭代器e之间的元素
d)可是使用push_front 头插
5. 顺序容器的删除操作
1. 删第一个或最后一个元素
类似与插入元素,pop_front或者pop_back可以删除第一个或者最后一个元素
2. 删除容器的一个元素
与insert对应,删除采用的是erase操作,该操作有两个版本:删除由一个迭代器指向的元素,或者删除由一对迭代器标记的一段元素。删除元素需要接收返回值,防止迭代器失效,最好使用while循环。
6. 容器大小的操作
vector与容量有关的函数:
a)size 元素数目,类似于会议室中人的数目
b)resize 调整元素数目,类似于调整函数
c)capacity 可容纳数目,类似于会议室中的座位数量
d)reserve 告诉vector容器应该预留多少个元素的存储空间
7. 迭代器的失效问题
任何insert或者push操作都可能导致迭代器失效。当编写循环将元素插入到vector或list容器中时,程序必须确保迭代器在每次循环后都得到更新。
vector迭代器持续有效,除非:
1. 使用者在较小的索引位置插入或者删除元素。
2. 由于容量的变化引起的内存重新分配。
list迭代器持续有效,除非:
将it指向的元素删除,那么it则失效(list内部实现是链表,it指向的元素删了就是没有了,再用it访问直接段错误。vector也有可能失效,只不过后面的元素会往前移,再用it访问可能不会产生段错误)。
删除元素需要接收返回值,最好使用while循环。例如删除下例删除偶数:
vector<int>::iterator it = vec.begin();
while(it != vec.end()) { if(*it % 2 == 0) //vec.erase(it); it = vec.erase(it); else ++it; }
8. vector的for_each方法
遍历vector方法:
1. 下标
2. 迭代器
3. for_each
#include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; void print(int i) { cout << i << endl; } int main(int argc, const char *argv[]) { vector<int> vec; vec.push_back(12); vec.push_back(23); vec.push_back(45); vec.push_back(56); vec.push_back(221); vec.push_back(35); vec.push_back(129); for_each(vec.begin(), vec.end(), print); return 0; } /* output: 12 23 45 56 221 35 129 */
9. vector和list的区别
a) vector采用数组实现,list采用链表。
b) vector支持随机访问,list不提供下标。
c) 大量增加删除的操作适合使用list。
10. string之截取子串substr
例子如下:
#include <iostream> #include <string> #include <vector> using namespace std; int main(int argc, const char *argv[]) { string s = "helloworldfoo"; string s2 = s.substr(1, 4); //ello cout << s2 << endl; return 0; }
注意,迭代器一般是取基地址到尾后地址的一段范围。而下标操作,通常是基地址+长度。
11. stack
#include <iostream> #include <string> #include <vector> #include <stack> using namespace std; int main(int argc, const char *argv[]) { stack<int> s; s.push(10); s.push(22); s.push(23); s.push(1); s.push(8); s.push(99); s.push(14); while(!s.empty()) { cout << s.top() << endl; s.pop(); } return 0; } /* 输出如下: 14 99 8 1 23 22 10 */
12. queue
#include <iostream> #include <string> #include <vector> #include <queue> using namespace std; int main(int argc, const char *argv[]) { queue<int> q; q.push(12); q.push(23); q.push(4); q.push(5); q.push(7); while(!q.empty()) { cout << q.front() << endl; q.pop(); } return 0; } /* 输出: 12 23 4 5 7 */
13. 优先级队列(用堆实现)
例1:
#include <iostream> #include <string> #include <vector> #include <queue> using namespace std; int main(int argc, const char *argv[]) { priority_queue<int> q; q.push(12); q.push(99); q.push(23); q.push(123); while(!q.empty()) { cout << q.top() << endl; q.pop(); } return 0; } /* output: 123 99 23 12 */
例2:
#include <iostream> #include <string> #include <vector> #include <queue> using namespace std; int main(int argc, const char *argv[]) { priority_queue<int, vector<int>, greater<int> > q; q.push(12); q.push(99); q.push(23); q.push(123); while(!q.empty()) { cout << q.top() << endl; q.pop(); } return 0; } /* output: 12 23 99 123 */
#include <iostream> #include <string> #include <vector> #include <queue> using namespace std; int main(int argc, const char *argv[]) { priority_queue<int, vector<int>, less<int> > q; q.push(12); q.push(99); q.push(23); q.push(123); while(!q.empty()) { cout << q.top() << endl; q.pop(); } return 0; } /* output: 123 99 23 12 */
例3:传入函数对象
#include <iostream> #include <string> #include <vector> #include <queue> using namespace std; struct Score { int score_; string name_; Score(int score, const string name) :score_(score), name_(name) { } }; class Cmp { public: bool operator() (const Score &s1, const Score &s2) { return s1.score_ < s2.score_; } }; // Cmp p; // p(s1, s2) int main(int argc, const char *argv[]) { priority_queue<Score, vector<Score>, Cmp> q; q.push(Score(67, "zhangsan")); q.push(Score(88, "lisi")); q.push(Score(34, "wangwu")); q.push(Score(99, "foo")); q.push(Score(0, "bar")); while(!q.empty()) { cout << q.top().name_ << " : " << q.top().score_ << endl; q.pop(); } return 0; } /* output: foo : 99 lisi : 88 zhangsan : 67 wangwu : 34 bar : 0 */
14. reverse迭代器
反向迭代器逻辑上指向的元素是物理上指向元素的下一个元素。
在实际(物理)实现上,rbegin()指向最后一个元素的下一个位置,rend()指向第一个元素。但是在逻辑上,rbegin()指向最后一个元素,rend()指向第一个元素的前一个位置。
注意:reverse迭代器不能用于erase函数。删除的正确方式是:it = string::reverse_iterator(s.erase((++it).base()));(见示例3)
示例1:
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main(int argc, const char *argv[]) { vector<int> coll; for (int i = 0; i <= 9 ; i++) // 0 1 2 3 4 5 6 7 8 9 { coll.push_back(i); } vector<int>::iterator pos; pos = find(coll.begin(), coll.end(), 5); // 此时pos物理指向的元素就是5 cout << "pos: " << *pos << endl; // 输出5 vector<int>::reverse_iterator rpos(pos); // 反向迭代器物理上指向的元素确实是5 cout << "rpos: " << *rpos << endl; // 但是逻辑上指向的元素是它的下一个元素,在此处即为4 } /* output: pos: 5 rpos: 4 */
示例2:
#include <iostream> #include <vector> #include <algorithm> using namespace std; void print(int i) { cout << i << " "; } int main(int argc, const char *argv[]) { vector<int> coll; for (int i = 0; i <= 9 ; i++) // 0 1 2 3 4 5 6 7 8 9 { coll.push_back(i); } vector<int>::iterator pos1; pos1 = find(coll.begin(), coll.end(), 2); // pos1指向2 vector<int>::iterator pos2; pos2 = find(coll.begin(), coll.end(), 7); // pos2指向7 for_each(pos1, pos2, print); // 输出2 3 4 5 6 cout << endl; vector<int>::reverse_iterator rpos1(pos1); // rpos1物理指向2,逻辑指向1 vector<int>::reverse_iterator rpos2(pos2); // rpos2物理指向7,逻辑指向6 for_each(rpos2, rpos1, print); // 输出6 5 4 3 2 cout << endl; } /* output: 2 3 4 5 6 6 5 4 3 2 */
示例3:
#include <iostream> #include <string> #include <vector> using namespace std; int main(int argc, const char *argv[]) { string s = "helloworld"; string::reverse_iterator it = s.rbegin(); // s.rbegin物理指向的元素最后一个元素之后的位置,逻辑指向的是最后一个元素 while(it != s.rend()) { if(*it == 'r') { string::iterator tmp = (++it).base(); // 由于earse()参数不能是删除反向迭代器,因此需要将其转换为正向迭代器 tmp = s.erase(tmp); // 而it此时物理指向的元素并不是'r',++it后才物理指向‘r’,此时经base()转换为正向迭代器后删除 it = string::reverse_iterator(tmp); // 之后将正向迭代器转换成反向迭代器 //it = string::reverse_iterator(s.erase((++it).base())); } else ++it; } cout << s << endl; } /* output: hellowold */
STL之关联容器
1. Pair类型
Pair是一种简单的关联类型。注意:pair不是容器,而是代表一个key-value键值对。
示例1:
#include <iostream> #include <string> #include <utility> using namespace std; int main(int argc, const char *argv[]) { pair<int, int> p1; p1.first = 10; p1.second = 12; pair<int, string> p2; p2.first = 12; p2.second = "hello"; pair<string, string> p3; }
示例2:
#include <iostream> #include <string> #include <vector> using namespace std; //生成pair对象的三种方法 int main(int argc, const char *argv[]) { vector<pair<string, int> > vec; pair<string, int> word; word.first = "hello"; word.second = 12; vec.push_back(word); pair<string, int> word2("world", 12); vec.push_back(word2); vec.push_back(make_pair("foo", 3)); }
示例3:vector中装入pair,实现统计词频:
#include <iostream> #include <string> #include <vector> #include <utility> using namespace std; typedef vector<pair<string, int> > Dict; void makeDict(Dict &dict, const vector<string> &words); void addWordToDict(Dict &dict, const string &word); int main(int argc, const char *argv[]) { vector<string> words; string word; while(cin >> word) words.push_back(word); Dict dict; makeDict(dict, words); for(const pair<string, int> &p : dict) { cout << p.first << " : " << p.second << endl; } return 0; } void makeDict(Dict &dict, const vector<string> &words) { dict.clear(); for(vector<string>::const_iterator it = words.begin(); it != words.end(); ++it) { addWordToDict(dict, *it); } } void addWordToDict(Dict &dict, const string &word) { Dict::iterator it; for(it = dict.begin(); it != dict.end(); ++it) { if(it->first == word) { ++it->second; break; } } if(it == dict.end()) dict.push_back(make_pair(word, 1)); }
2. map
map可以看做是一种存储pair类型的容器,内部采用二叉树实现(编译器实现为红黑树)。
1. pair不是容器,而是代表一个key-value键值对;而map则是一个容器,里面存储了pair对象,只是存储的方式与vector<pair>这种连续存储,有所不同,map采用的是二叉排序树存储pair,一般而言是红黑树,因此内部是有序的
2. 当map使用下标访问时,如果key不存在,那么会在map中添加一个新的pair,value为默认值
示例1:
#include <iostream> #include <string> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, int> m; m["beijing"] = 2000; m["shenzhen"] = 1000; m["shanghai"] = 1500; m["hongkong"] = 500; m["hangzhou"] = 880; for(map<string, int>::const_iterator it = m.begin(); it != m.end(); ++it) { //*it pair cout << it->first << " : " << it->second << endl; } return 0; } /* output: beijing : 2000 hangzhou : 880 hongkong : 500 shanghai : 1500 shenzhen : 1000 */
// 由于key是string类型,因此输出按字典序。
示例2:
#include <iostream> #include <string> #include <vector> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, int> m; m["beijing"] = 40; m["shenzhen"] = 30; m["guangzhou"] = 37; cout << m.size() << endl; //3 cout << m["shanghai"] << endl; cout << m.size() << endl; return 0; } /* output: 3 0 4 */
以下为错误代码:
#include <iostream> #include <map> using namespace std; struct Test { int a; }; int main(int argc, const char *argv[]) { map<Test, int> m; Test t; m[t] = 1; } /* 编译报错,因为Test对象在次数为key-value对中的key,但其并没有定义 operator< 运算符,红黑树无法进行排序 */
4. map查找元素的效率是lgn,因为树的高度不超过O(lgN)
示例:使用map,实现统计词频,如下:
#include <iostream> #include <string> #include <vector> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, int> words; string word; /* 如果key(word)存在,则value++; 如果word不存在,此处会在map(words)中添加一个新的pair,value为默认值(此处为0),然后value++ */ while(cin >> word) words[word]++; for(const pair<string, int> &p : words) cout << p.first << " : " << p.second << endl; return 0; }
5. 在map中添加元素
刚才我们看到,采用下标的方式,可以给map添加元素,但更好的做法时采用insert插入一个pair对象。
这里值得注意的是insert的返回值,其返回了一个pair对象,第一个元素是指向该key所在的那个pair对象的的迭代器,第二个则表示插入是否成功。使用insert插入map元素时,如果失败,则不会更新原来的值。看下面例子:
#include <iostream> #include <string> #include <vector> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, int> m; m.insert(make_pair("hello", 1)); m.insert(make_pair("foo", 1)); m.insert(make_pair("bar", 1)); m.insert(make_pair("hello", 1)); cout << "size : " << m.size() << endl; /* insert的返回值:指向key所在pair的迭代器,以及表示插入是否成功的布尔值 */ pair<map<string, int>::iterator, bool> ret; // 之前没有这个key,插入成功 ret = m.insert(make_pair("fwfgwfg", 23)); cout << "ret = " << ret.second << endl; // 之前已有的key,插入失败。插入失败的话,不会更新原来的value值 ret = m.insert(make_pair("hello", 25425)); cout << "ret = " << ret.second << endl; cout << ret.first->second << endl; return 0; } /* output: size : 3 ret = 1 ret = 0 1 */
下面的程序仍然是实现统计词频:
#include <iostream> #include <string> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, int> words; string word; pair<map<string, int>::iterator, bool> ret; while(cin >> word) { ret = words.insert(make_pair(word, 1)); if(ret.second == false) //word已经存在 ++ret.first->second; } for(const pair<string, int> &p : words) cout << p.first << " : " << p.second << endl; return 0; }
综上,在本章中我们已经使用三种方式,去统计词频了,分别是:vector中使用pair, map的下标访问方式以及map的insert方式。
6. 在map中查找元素
刚才看到可以利用下标获取value的值,但是这样存在一个弊端,如果下标访问的是不存在的元素,那么会自动给map增加一个键值对,这显然不是我们所预期的。
我们可以采用 count 和 find 来解决问题,其中 count 仅仅能得出该元素是否存在,而 find 能够返回该元素的迭代器。
示例1:
#include <iostream> #include <string> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, string> m; m["beijing"] = "bad"; m["shanghai"] = "just soso"; m["shenzhen"] = "well"; m["hangzhou"] = "good"; cout << m.count("hangzhou") << endl; cout << m.count("HK") << endl; return 0; } /* output: 1 0 */
示例2:
#include <iostream> #include <string> #include <map> using namespace std; int main(int argc, const char *argv[]) { map<string, string> m; m["beijing"] = "bad"; m["shanghai"] = "just soso"; m["shenzhen"] = "well"; m["hangzhou"] = "good"; // find的返回值 map<string, string>::iterator it = m.find("HK"); if(it == m.end()) cout << "不存在" << endl; else cout << it->first << " : " << it->second << endl; return 0; } /* output: 不存在 */
3. set
Set类似于数学上的集合,仅仅表示某个元素在集合中是否存在,而不必关心它的具体位置。同样,set中的元素互异,也就是无法两次插入相同的元素。set 底层采用红黑树实现,按照值进行排序,map则按照key进行排序。使用方式和map类似,但是简单很多。
示例1:
#include <iostream> #include <string> #include <set> using namespace std; int main(int argc, const char *argv[]) { set<int> s; // set不会插入重复的元素 for(int i = 0; i < 20 ; ++i) { s.insert(i); s.insert(i); } cout << "size : " << s.size() << endl; return 0; } /* output: size : 20 */
示例2:
#include <iostream> #include <string> #include <vector> #include <set> #include <stdlib.h> using namespace std; int main(int argc, const char *argv[]) { srand(10000); set<int> s; for(int i = 0; i < 40; ++i) { s.insert(rand() % 100); } // 注意是有序的 for(int i : s) { cout << i << " "; } cout << endl; return 0; } /* output: 4 5 8 12 13 15 16 20 21 22 24 25 27 32 38 39 42 43 46 50 54 57 59 63 66 72 78 82 85 93 94 96 98 */
4. 小结
map 中key 的值是唯一的,set 中的元素都是唯一的。
1. map和set比较:
a) 二者均使用红黑树实现
b) key需要支持<操作
c) map侧重于key-value的快速查找
d) set侧重于查看元素是否存在
2. 用户无法对map和set中的元素进行排序,不然会干扰map和set本身的实现。
最后注意:map 的 key 值是不可更改的。而 set 中的 value 虽然可以更改,但不建议这样做,真有需求,直接删除即可。
5. 哈希
c++11标准添加了 std::unordered_map 与 std::unordered_map。
map采用二叉树实现,hash_map采用hash表,那么二者的使用上:
a) 当key本身需要排序时,使用map
b) 其他情况采用hash_map更佳(hash_map无序),但是采用map效率也是足够的。
#include <iostream> #include <string> #include <vector> #include <unordered_map> using namespace std; int main(int argc, const char *argv[]) { unordered_map<string, int> m; m["beijing"] = 1; m["shanghai"] = 2; m["shenzhen"] = 3; for(unordered_map<string, int>::const_iterator it = m.begin(); it != m.end(); ++it) { cout << it->first << " : " << it->second << endl; } } /* output: shenzhen : 3 shanghai : 2 beijing : 1 */
STL 算法
以下只是对小部分常用算法进行了介绍,但具体用法大同小异。
1. for_each
template <class InputIterator, class Function> Function for_each (InputIterator first, InputIterator last, Function fn);Apply function to range
Applies function fn to each of the elements in the range
[first,last)
.
#include <iostream> #include <string> #include <vector> #include <list> #include <algorithm> #include <ctype.h> using namespace std; void toUpper(string &s) { for(string::iterator it = s.begin(); it != s.end(); ++it) { if(islower(*it)) *it = toupper(*it); } } void print(const string &s) { cout << s << " "; } int main(int argc, const char *argv[]) { vector<string> vec; vec.push_back("beijing"); vec.push_back("changchun"); vec.push_back("shijiahzuang"); vec.push_back("shenyang"); vec.push_back("dalian"); vec.push_back("jinan"); vec.push_back("nanjing"); for_each(vec.begin(), vec.end(), toUpper); for_each(vec.begin(), vec.end(), print); } /* output: BEIJING CHANGCHUN SHIJIAHZUANG SHENYANG DALIAN JINAN NANJING */
2. count && count_if
count
template <class InputIterator, class T> typename iterator_traits<InputIterator>::difference_type count (InputIterator first, InputIterator last, const T& val);Count appearances of value in range
Returns the number of elements in the range
[first,last)
that compare equal to val.
The function usesoperator==
to compare the individual elements to val.
示例:
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main(int argc, const char *argv[]) { int myints[] = {10,20,30,30,20,10,10,20}; int mycount = count(myints, myints+8, 10); cout << "10 appears " << mycount << " times." << endl; vector<int> myvector(myints, myints+8); mycount = count(myvector.begin(), myvector.end(), 20); cout << "20 appears " << mycount << " times." << endl; } /* output: 10 appears 3 times. 20 appears 3 times. */
count_if
template <class InputIterator, class Predicate> typename iterator_traits<InputIterator>::difference_type count_if (InputIterator first, InputIterator last, UnaryPredicate pred);Return number of elements in range satisfying condition
Returns the number of elements in the range
[first,last)
for which pred is true.
示例:
/* count_if example */ #include <iostream> #include <vector> #include <algorithm> using namespace std; bool IsOdd(int i) { return i % 2 == 1; } int main(int argc, const char *argv[]) { vector<int> vec; for(int i = 1; i < 10; ++i) { vec.push_back(i); //vec: 1 2 3 4 5 6 7 8 9 } int mycount = count_if(vec.begin(), vec.end(), IsOdd); cout << "vec contains " << mycount << " odd values." << endl; } /* output: vec contains 5 odd values. */
3. min_element
default (1)
template <class ForwardIterator> ForwardIterator min_element (ForwardIterator first, ForwardIterator last);custom (2)
template <class ForwardIterator, class Compare> ForwardIterator min_element (ForwardIterator first, ForwardIterator last, Compare comp);Return smallest element in range
Returns an iterator pointing to the element with the smallest value in the range
[first,last)
.The comparisons are performed using either
operator<
for the first version, or comp for the second; An element is the smallest if no other element compares less than it. If more than one element fulfills this condition, the iterator returned points to the first of such elements.
示例:
// min_element/max_element example #include <iostream> // std::cout #include <algorithm> // std::min_element, std::max_element bool myfn(int i, int j) { return i<j; } struct myclass { bool operator() (int i,int j) { return i<j; } } myobj; int main () { int myints[] = {3,7,2,5,6,4,9}; // using default comparison: std::cout << "The smallest element is " << *std::min_element(myints,myints+7) << ' '; std::cout << "The largest element is " << *std::max_element(myints,myints+7) << ' '; // using function myfn as comp: std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myfn) << ' '; std::cout << "The largest element is " << *std::max_element(myints,myints+7,myfn) << ' '; // using object myobj as comp: std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myobj) << ' '; std::cout << "The largest element is " << *std::max_element(myints,myints+7,myobj) << ' '; return 0; } /* output: The smallest element is 2 The largest element is 9 The smallest element is 2 The largest element is 9 The smallest element is 2 The largest element is 9 */
4. find && find_if
find
template <class InputIterator, class T> InputIterator find (InputIterator first, InputIterator last, const T& val);Find value in range
Returns an iterator to the first element in the range
[first,last)
that compares equal to val. If no such element is found, the function returns last.The function uses
operator==
to compare the individual elements to val.
find_if
template <class InputIterator, class UnaryPredicate> InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred);Find element in range
Returns an iterator to the first element in the range
[first,last)
for which pred returnstrue
. If no such element is found, the function returns last.
示例:
#include <iostream> #include <string> #include <vector> #include <list> #include <algorithm> #include <ctype.h> using namespace std; void print(const string &s) { cout << s << " "; } bool isShorter(const string &s) { return s.size() < 6; } int main(int argc, const char *argv[]) { vector<string> vec; vec.push_back("beijing"); vec.push_back("changchun"); vec.push_back("shijiahzuang"); vec.push_back("shenyang"); vec.push_back("dalian"); vec.push_back("jinan"); vec.push_back("nanjing"); vector<string>::iterator it = std::find(vec.begin(), vec.end(), "dalian"); cout << *it << endl; //find_if it = std::find_if(vec.begin(), vec.end(), isShorter); cout << *it << endl; } /* output: dalian jinan */
5. copy
template <class InputIterator, class OutputIterator> OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result);Copy range of elements
Copies the elements in the range
[first,last)
into the range beginning at result.The function returns an iterator to the end of the destination range (which points to the element following the last element copied).
The ranges shall not overlap in such a way that result points to an element in the range [first,last). For such cases, see copy_backward.
示例(注意插入迭代器的用法):
#include <iostream> #include <string> #include <vector> #include <list> #include <algorithm> #include <ctype.h> using namespace std; void print(const string &s) { cout << s << " "; } int main(int argc, const char *argv[]) { vector<string> vec; vec.push_back("beijing"); vec.push_back("changchun"); vec.push_back("shijiahzuang"); vec.push_back("shenyang"); vec.push_back("dalian"); vec.push_back("jinan"); vec.push_back("nanjing"); list<string> lst; std::copy(vec.begin(), vec.end(), back_inserter(lst)); //执行的是push_back。如果填写lst.begin(),需要list<string> lst(7); for_each(lst.begin(), lst.end(), print); cout << endl; lst.clear(); std::copy(vec.begin(), vec.end(), front_inserter(lst)); //执行的是push_front。如果填写lst.rbegin(),需要list<string> lst(7); for_each(lst.begin(), lst.end(), print); cout << endl; return 0; } /* output: beijing changchun shijiahzuang shenyang dalian jinan nanjing nanjing jinan dalian shenyang shijiahzuang changchun beijing */
6. lambda 表达式
c++11中新增了lambda表达式。
简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。
示例:
#include <iostream> #include <string> #include <vector> #include <algorithm> #include <ctype.h> using namespace std; void toUpper(string &s) { for(string::iterator it = s.begin(); it != s.end(); ++it) { if(islower(*it)) *it = toupper(*it); } } void print(const string &s) { cout << s << " "; } int main(int argc, const char *argv[]) { vector<string> vec; vec.push_back("beijing"); vec.push_back("changchun"); vec.push_back("shijiahzuang"); vec.push_back("shenyang"); vec.push_back("dalian"); vec.push_back("jinan"); vec.push_back("nanjing"); for_each(vec.begin(), vec.end(), toUpper); for_each(vec.begin(), vec.end(), [](const string &s) { cout << s << " "; } ); } /* output: BEIJING CHANGCHUN SHIJIAHZUANG SHENYANG DALIAN JINAN NANJING */
OOP之复制控制
1. 对象复制的时机:
a)根据一个类去显式或者隐式初始化一个对象
b)复制一个对象,将它作为实参传给一个函数
c)从函数返回时复制一个对象
那么如何完成对象复制的工作?这里需要的就是拷贝构造函数。
2. 拷贝构造函数(也叫复制构造函数)
只有单个形参,而且该形参是本类类型对象的引用(常用const修饰),这样的构造函数成为复制控制函数。
复制构造函数调用的时机就是在对象复制的时候。
如果什么也不做,编译器会自动帮我们合成一个默认的复制构造函数。
那么如果我们自己来定义复制构造函数,应该怎么写?示例如下:
#include <iostream> #include <string> #include <vector> using namespace std; class Student { public: Student() {} Student(int id, const string &name, int age) :id_(id), name_(name), age_(age) { } Student(const Student &other) :id_(other.id_), name_(other.name_), age_(other.age_) { } void print() const { cout << id_ << " : " << name_ << " : " << age_; } private: int id_; string name_; int age_; }; int main(int argc, const char *argv[]) { Student s(11, "zhangsan", 23); s.print(); cout << endl; Student s2(s); // 调用拷贝构造函数 s2.print(); cout << endl; } /* output: 11 : zhangsan : 23 11 : zhangsan : 23 */
现在来思考一个问题,既然编译器生成的拷贝构造函数工作正常,那么什么时候需要我们自己来编写拷贝构造函数呢?这就是下面的深拷贝和浅拷贝的问题。
3. 深拷贝和浅拷贝
我们通过自己定义的string类来解释深拷贝与浅拷贝的问题。先来看以下这个错误版本的string类:
_string.h
#ifndef _STRING_H_ #define _STRING_H_ #include <stddef.h> namespace __str { class string { public: string(); string(const char*); void debug() const; size_t size() const; ~string(); private: char *_str; }; } /* namespace __str */ #endif /*_STRING_H_*/
_string.cpp
#include "_string.h" #include <iostream> #include <string.h> using namespace std; namespace __str { string::string() :_str(new char[1]) { _str[0] = 0; } string::string(const char *s) :_str(new char[strlen(s) + 1]) { strcpy(_str, s); } size_t string::size() const { return strlen(_str); } void string::debug() const { cout << _str << endl; } string::~string() { delete []_str; }
} /* namespace __str */
main.cpp
#include "_string.h" using namespace __str; int main(int argc, const char *argv[]) { string s("hello"); // 调用一个参数的构造函数 s.debug(); string s2(s); // 调用系统合成的拷贝构造函数 s2.debug();
}
程序运行后,输出两次 hello,直接直接挂掉。为什么会这样子呢?
因为系统合成的拷贝构造函数,在复制String对象时,只是简单的复制其中的_str的值,这样复制完毕后,就有两个String中的_str指向同一个内存区域,当对象析构时,发生两次delete,导致程序错误
如何解决?
方案很简单,就是我们在复制String时,不去复制str的值,而是复制其指向的内存区域。
我们自定义拷贝构造函数如下:
string::string(const String &s) :str_(new char[strlen(s.str_) + 1]) { strcpy(str_, s.str_); }
如此程序就可以正常运行,输出两次hello。
含有指针成员变量的类在复制时,有两种选择:
a) 复制指针的值,这样复制完毕后,两个对象指向同一块资源,这叫做浅拷贝 shallow copy
b) 复制指针所指向的资源,复制完毕后,两个对象各自拥有自己的资源,这叫做深拷贝 deep copy
注意:编译器默认的是浅拷贝,此时如果需要深拷贝,需要自己编写拷贝构造函数。
4. 赋值运算符
前面的复制构造函数说的是对象的复制,对象的赋值调用的则是对象的赋值运算符。
对于我们自定义的类(例如Student),我们是无法进行比较操作的,因为我们自定义的类没有内置比较运算符(<= < > >= == !=),此时我们就可以通过运算符重载的规则给这些类加上运算符,这里我们需要重载的就是赋值运算符。
当然,如果我们什么也不做,系统也会自动合成一个赋值运算符,但是什么时候需要我们自己来重载赋值运算符呢,仍然是考虑深拷贝和浅拷贝的问题。
对于刚刚我们自定义的string类这个例子而言,如果我们使用系统自动合成的赋值运算符,那么同样会引起错误。因为当发生赋值时,两个string对象的_str仍然会指向同一片内存空间,那么当程序退出时,会析构两次,发生错误。
因此,我们应该自己来定义string类的赋值运算符,如下:
string& string::operator=(const string &s)// 自定义赋值运算符 { // 防止自赋值,这样执行delete的时候,会冲掉原有内容 if(this == &s) { return *this; }
// 释放原来指向的内存空间 delete []_str;
_str = new char[strlen(s._str) + 1]; strcpy(_str, s._str); return *this; }
注意:赋值操作符,需要先释放掉以前持有的资源,同时必须处理自赋值的问题。
5. 禁止类的复制和赋值
如果想禁止复制一个类,应该怎么办?
显然需要把类的复制构造函数设为private,但是这样以来类的friend仍然可以复制该类,于是我们只声明这个函数,而不去实现。另外,如果你不需要复制该类的对象,最好把赋值运算也一并禁用掉。
所以这里的做法是:把复制构造函数和赋值运算符的声明设为private而不去实现。
注意:如果一个类,不需要复制和赋值,那就禁用这种能力,这可以帮助避免大量潜在的bug。
示例:
class Test { public: Test() {} ~Test() {} private: Test(const Test &t); void operator=(const Test &t); };
实际上,更通用的做法是写一个类noncopyable,凡是继承该类的任何类都无法复制和赋值。
而google开源项目风格指南建议的做法是使用 DISALLOW_COPY_AND_ASSIGN 宏:
// 禁止使用拷贝构造函数和 operator= 赋值操作的宏 // 应该类的 private: 中使用 #define DISALLOW_COPY_AND_ASSIGN(TypeName) TypeName(const TypeName&); void operator=(const TypeName&)
在 class foo 中使用方式如下:
class Foo { public: Foo(int f); ~Foo(); private: DISALLOW_COPY_AND_ASSIGN(Foo); };
绝大多数情况下都应使用 DISALLOW_COPY_AND_ASSIGN 宏。如果类确实需要可拷贝,应在该类的头文件中说明原由,并合理的定义拷贝构造函数和赋值操作。注意在 operator= 中检测自我赋值的情况。为了能作为 STL 容器的值,你可能有使类可拷贝的冲动。在大多数类似的情况下,真正该做的是把对象的指针放到 STL 容器中。可以考虑使用智能指针。
6. 小结
1. 复制构造函数、赋值运算符以及析构函数,称为三法则,一旦提供了其中一个,务必提供其余两个。以我们之前自定义的string类为例:
a) 涉及到深拷贝、浅拷贝问题,所以需要提供拷贝构造函数
b) 然后,为了保持一致,赋值运算符也应该实现深拷贝
c) 既然实现深拷贝,那么必定申请了资源(例如内存),所以必然需要析构函数来手工释放。
2. 一个空类,编译器提供默认无参数构造函数、拷贝构造函数、赋值运算符以及析构函数,一共四个函数(针对03标准,c++11中还有移动构造函数和移动赋值运算符)。
3. 对于复制和赋值,请务必保证在程序的语义上具有一致性。
4. 如果一个类,实现了像value一样的复制和赋值能力(意味着复制和赋值后,两个对象没有任何关联,或者逻辑上看起来无任何关联),那么就称这个类的对象为值语义(value semantics)。如果类不能复制,或者复制后对象之间的资源归属纠缠不清,那么称为对象语义(object semantics),或者引用语义(reference semantics)。
运算符重载
1. 通过自定义运算符,程序员可以自定义类的操作
运算符的重载有成员函数和友元两种形式。有的运算符可以选择任意一种实现,有的则必须使用友元函数的形式。
2. 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多
一员运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧对象传递给第二个参数。如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。
3. 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
4. 准则
下面的准则有助于我们在将运算符定义为成员函数还是右元函数做出抉择:
1. 赋值(=)、下标([ ])、调用(( ))和成员访问箭头(->)运算符必须是成员。
2. 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
3. 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
4. 具有对称性的运算符可能转换成任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是友元函数。
程序员希望能在含有混合类型的表达式中使用对称性运算符。例如,我们能求一个int和一个double的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数(通常为右元形式)。
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。例如:
string s = "world";
string t = s + "!"; // 正确:我们能把一个const char* 加到一个string对象中 string u = "hi" + s; // 如果+是string的成员函数,则编译报错
如果 operator+ 是 string 类的成员,则上面的第一个加法等价于 s.operator(“!”) 。同样的,“hi”+ s 等价于 “hi”.operator+(s)。显然“hi”的类型是const char*,这是一种内置类型,根本没有成员函数。
因为标准库的 string 类将+定义成了普通的非成员函数,所以 “hi”+ s 等价于operator+(“hi”,s)。和任何其他函数一样,每个实参都能被转换成形参类型。唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地抓换成string。
5. 两个注意点
1. 区分前置和后置运算符
要想同时定义前置和后置运算符,必须首先解决一个问题,即普通的重载形式无法区分这两种情况。前置和后置版本使用的是同一个符号,意味着其重载版本所用的名字将是相同的,并且运算对象的数量和类型也相同。
为了解决这个问题,后置版本使用一个额外的(不被使用)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个形参的唯一作用就是区分前置版本和后置版本的函数,而不是真正要在实现后置版本时参与运算。
示例:
class A { public: A operator++(int); //后置运算符 A operator--(int); }; // 注意前置运算符返回的是引用(左值) // 后置运算符返回的是临时变量(右值) A A::operator++(int) { A ret = *this; // 记录当前的值 ++*this; // 假设前置++已定义 return ret; // 返回之前记录的状态 } A A::operator--(int) { A ret = *this; --*this; return ret; } // 如果想通过函数调用的方式调用后置版本,必须为它的整型参数传递一个值。 // 尽管传入的值通常会被运算符函数忽略,但却必不可少,因为编译器只有通过它才能知道应该使用后置版本 A p; p.operator++(0); //调用后置版本的operator++ p.operator++(); //调用前置版本的operator++
这里提一下与本块内容不是很相关的两个易错点:
a)static成员在类的定义体中声明为static即可,类外定义无需再加上static。
b)指定默认形参的值只需在声明中即可,定义中无需再指明。
2. 对箭头运算符返回值的限定
对于其他运算符,我们可以指定它做任何事情。但是对于箭头运算符,它永远不能丢掉成员访问这个最基本的含义。
当我们重载箭头运算符时,可以改变的是从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变。
箭头运算符最后返回的永远是指针!看如下代码:
point -> men // 等价于point.operator->()->men;
综上:重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
6. 源码
这里通过String类的编写,讲述运算符的重载的实际运用。此处代码过长,请读者去我的github上阅读,地址为https://github.com/jianxinzhou/classHub/tree/master/string。
OOP之泛型编程
1. 函数模板
1. 函数模板可以看做一种代码产生器,往里面放入具体的类型,得到具体化的函数。
2. 模板的编译分为两步:
a) 实例化之前,先检查模板本身语法是否正确。
b) 根据函数调用,去实例化代码,产生具体的函数。
3. 没有函数调用,就不会实例化模板代码,在目标文件 obj 中找不到模板的痕迹。
4. 一个非模板函数可以和一个同名的函数模板同时存在,构成,同样,同名的两个模板函数之间也可以因为参数不同构成重载。
5. 模板函数重载时,选择函数版本的一些特点:
a) 当条件相同时,优先选择非模板函数。
b) 在强制类型转化,与实例化模板可行之间,优先选择实例化模板。
c) 实例化版本不可行,则去尝试普通函数的转化。
d) 参数是指针时,优先选择指针版本。
e) 总之,尽可能采用最匹配的版本。
6. 在模板函数重载中,不要混合使用传值和传引用。尽可能使用传引用。
7. 传值和传引用对于参数来说,本质区别在于是否产生了局部变量。
8. 对于返回值而言,传值和传引用的区别在于,返回时是否产生了临时变量。
9. 函数的所有重载版本的声明都应该位于该函数被调用的位置之前。
示例代码参看:https://github.com/jianxinzhou/classHub/tree/master/S_Template/fun_template
2. 类模板
1. 模板类类似于代码产生器,根据用户输入的类型不同,产生不同的class。
2. 标准库中的vector就是一个典型的模板类,vector<int> 和 vector<string>是两个完全不同的类。同样,vector不是一个完整的类名。
3. 在模板类的内部,可以直接使用模板类名作为完整类名,而不必指定抽象类型T,例如 vector 内部可以直接使用 vector,而不必使用 vector<T>。
4. 模板类的编译也分为两步:
a) 检查模板class的自身语法
b) 根据用户的指定类型vector<string>,去实例化一个模板类。注意,不是实例化所有的代码,而是仅仅实例化用户调用的部分。
5. 模板类看做函数,输入的是类型,输出的是具体的class代码,所以模板类是一个代码产生器。
6. 模板的缺点是代码膨胀,编译速度慢。带来的好处是运行速度快。
7. 在类的外面实现函数时,注意类名要写完整,例如Stack<T>
8. 将Stack拆分成h和cpp文件,构建时产生了链接错误,原因在于:
a) 模板的调用时机和代码的实例化必须放在同一时期。
b) 编译Stack.cpp时,编译器找不到任何用户调用的代码,所以得到的 Stack.o文件为空,可以使用nm -A查看。
c) 编译main.cpp时,编译器获取用户的调用,了解应该去实例化哪些代码(pop push),但是这些代码存在于另一模块,编译器无法实例化(#include进来的.h文件只有声明)。
d) 链接期间,因为以上的原因,需要链接的代码并没有产生,找不到pop、 push等函数的代码,报错。
因此比较简单的做法是:我们要把类的定义和实现全部放到同一个文件中,可以为 .h 文件,但最好使用 .hpp 文件。
示例代码参看:https://github.com/jianxinzhou/classHub/tree/master/S_Template/class_template
3. 缺省模板实参
1. 对于类模板,你还可以为模板参数定义缺省值,这些值就被称为缺省模板实参。而且,它们还可以引用之前的模板参数。
2. 实际上,STL中的stack、queue、priority_queue等并不是容器,而是根据其他容器适配而来的适配器(adapter),例如 stack:
template <class T, class Container = deque<T> > class stack;
stack 默认采用 deque 作为底层实现,但是用户可以自行制定容器。
3. STL中的容器大多使用了缺省模板参数,例如 map:
template < class Key, // map::key_type class T, // map::mapped_type class Compare = less<Key>, // map::key_compare class Alloc = allocator<pair<const Key,T> > // map::allocator_type > class map;
4. 示例代码参看:
https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E7%BC%BA%E7%9C%81%E6%A8%A1%E6%9D%BF%E5%AE%9E%E5%8F%82
4. 模板参数不仅可以为类型,还可以为数值
1. 值得注意的是:数值也是类名的一部分,例如 Stack<int, 5> 和 Stack<int, 10> 不是同一个类,二者的对象也无法相互赋值。
2. 为了解决上述的问题,可以使用成员模板,实现 Stack<int, 5> 和 Stack<int, 10>,甚至是和 Stack<double, 12> 之间的赋值,方法就是在 Stack 模板内部编写:
template <typename T2, int MAXSIZE2> Stack<T, MAXSIZE> &operator=(const Stack<T2, MAXSIZE2> &other);
注意这个函数的存在,并不影响编译器为我们提供默认的赋值运算符(默认的赋值运算符是同类型的)。
3. 具体代码参看:
https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E6%88%90%E5%91%98%E6%A8%A1%E6%9D%BF
5. 在模板定义内部指定类型
除了定义数据成员或函数成员之外,类还可以定义类型成员。例如,标准库的容器类定义了不同的类型,如 size_type,使我们能够以独立于机器的方式使用容器。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。例如,在模板中编写:
T::value_type * p;
1. 编译器可能将其解释为乘法,为了显式告诉编译器这是定义一个变量,需要加上typename
2. typename T::value_type * p;
3. 示例代码如下:
#include <iostream> #include <string> #include <vector> using namespace std; template <class Parm, class U> Parm fcn(Parm *array, U value)
{ typename Parm::size_type * p; return *array; } int main(int argc, const char *argv[]) { vector<int> vec; fcn(&vec, 12); return 0; }
6. 注意点
1. 对于非引用类型的参数,在实参演绎的过程中,会出现从数组到指针的类型转换,也称为衰退(decay)。
2. C中只有传值,所以C语言中把数组当做函数的参数,总是引发decay问题,丢失数组的长度信息。
3. 引用类型不会引发衰退(decay)。
4. 具体代码参看:
https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E8%A1%B0%E9%80%80
oop之内存分配
1. new & delete
C++中的 new 运算符,具体工作流程如下:
1. 调用 operator new 申请原始内存
2. 调用 place new 表达式,执行类的构造函数
3. 返回内存地址
而 delete 操作符的工作是:
1. 调用对象的析构函数
2. 调用 operator delete 释放内存
注意:free 干的事情是释放内存,delete 干的事情是调用析构函数,然后释放内存。
示例代码如下:
#include <iostream> using namespace std; class Test { public: Test() { cout << "Test" << endl; } ~Test() { cout << "~Test" << endl; } }; int main(int argc, char const *argv[]) { //这里的pt指向的是原始内存 Test *pt = static_cast<Test*>(operator new[] (5 * sizeof(Test))); for(int ix = 0; ix != 5; ++ix) { new (pt+ix)Test(); //调用定位new运算式 执行构造函数 } for(int ix = 0; ix != 5; ++ix) { pt[ix].~Test(); //调用析构函数,但是并未释放内存 } operator delete[] (pt); //释放内存 }
2. 标准库Allocate
c++中 new 运算符涉及到的工作无非以下两步:
1. 申请原始内存
2. 执行构造函数
delete 涉及到了两个工作:
1. 执行析构函数
2. 释放原始内存
实际上,标准库提供了一种更加高级的手段实现内存的分配和构造,下面我们介绍 std::allocator<T>。
对应于上面的 new 和 delete , allocator 提供了如下四个操作:
a.allocate(num) 为 num 个元素分配原始内存
a.construct(p) 将 p 所指的元素初始化(执行构造函数)
destroy(p) 销毁 p 指向的元素 (执行析构函数)
deallocate(p, num) 回收 p 指向的“可容纳 num 个元素”的内存空间
来看如下示例:
#include <iostream> #include <string> #include <vector> #include <memory> using namespace std; class Test { public: Test() { cout << "Test" << endl; } ~Test() { cout << "~Test" << endl; } Test(const Test &t) { cout << "Copy..." << endl; } }; int main(int argc, const char *argv[]) { allocator<Test> alloc; // 此时pt指向的是原始内存 Test *pt = alloc.allocate(3); // 申请3个单位的Test内存 { // 构建一个对象,使用默认值 // 注意调用的是拷贝构造函数 alloc.construct(pt, Test()); alloc.construct(pt+1, Test()); alloc.construct(pt+2, Test()); } // 执行指针所指对象的析构函数 alloc.destroy(pt); alloc.destroy(pt+1); alloc.destroy(pt+2); // 释放原始内存 alloc.deallocate(pt, 3); return 0; } /* output: Test 注意Test与~Test是临时对象执行构造函数和析构函数的结果 Copy... ~Test Test Copy... ~Test Test Copy... ~Test ~Test ~Test ~Test */
这里注意,allocator提供的 allocate 函数与 operator new 函数区别在于返回值,前者返回的是指向要分配对象的指针,而后者返回的是 void *,所以前者更加安全。
还有一点,construct一次只能构造一个对象,而且调用的是拷贝构造函数。实际上,标准库提供了三个算法用于批量构造对象(前提是已经分配内存),如下:
uninitialized_fill(beg, end, val) // 以val初始化[beg, end]
uninitialized_fill_n(beg, num, val) // 以val初始化beg开始的num个元素
uninitialized_copy(beg, end, mem) // 以[beg, end)的各个元素也初始化mem开始的各个元素
以上三个函数操控的对象都是原始内存,示例如下:
#include <iostream> #include <string> #include <vector> #include <memory> #include <stdlib.h> using namespace std; class Test { public: Test(int val) :val_(val) { cout << "Test ..." << endl; } ~Test() { cout << "~Test ..." << endl; } Test(const Test &t) :val_(t.val_) { cout << "Copy ... " << endl; } // just for test, so do as a public member int val_; }; int main(int argc, const char *argv[]) { // 利用malloc申请原始内存 Test *pt = (Test*)malloc(3 * sizeof(Test)); Test t(12); uninitialized_fill(pt, pt+3, t); cout << pt[0].val_ << endl; Test *pt2 = (Test*)malloc(2 * sizeof(Test)); uninitialized_copy(pt, pt+2, pt2); free(pt); free(pt2); return 0; } /* output: Test ... Copy ... Copy ... Copy ... 12 Copy ... Copy ... ~Test ... */
注意,以上示例中,free 只会释放空间,并不会执行析构函数。
这里注意标准库的 copy、fill 函数与 uninitialized_ 系列函数的区别:
copy、fill 等操作的是已经初始化对象的内存,因此调用的是赋值运算符
而uninitialized_针对的是原始内存,调用的是拷贝构造函数
至此,我们可以总结出分配原始内存的三种手段:
1. 使用malloc
2. 使用operator new
3. allocator的allocate函数
这三者从上到下,是一个由低级到高级的过程。
那么执行构造函数,有两种手段:
1. 使用placement new运算符
2. 使用allocator的construct函数
3. 小结
1. POD 数据仅仅申请内存就可以直接使用,不需要执行特殊的构造工作(指执行构造函数),可以直接使用malloc。因此,对于POD数据,可以通过memcpy系列函数,直接操控内存达到目的。 注意,C语言中的数据都是POD类型。
2. C++中的非POD变量经过两个步骤生成:
a) 申请原始内存(字节数组)。
b) 在内存上执行构造函数。
因此,对于非POD数据通常使用 new 一步到位,而不使用malloc。
3. 切记:allocator 执行 construct 时调用的是拷贝构造函数。
4. 总之,C语言中的数据都是 POD 类型,使用原始内存即可,但是C++中的大部分都是POD类型,需要执行相应的初始化函数,所以,在C++中应该尽可能避免使用memcpy之类的直接操控原始内存的函数。
5. POD指的是原生数据,包括int、double等基本数据,以及包含基本数据的结构体(struct或class),但是 class 或者 strut 不能包含自定义的构造函数,不能含有虚函数、更不能包含非POD数据。这个定义并不精确,但是够用。可以参看我的这篇博文,Aggregate类型以及值初始化。
oop之继承
之所以把继承放在模板之后来说,是因为模板与泛型编程这块博大精深,真正要精通的话,还得去看 C++ Templates 这本经典的传世之作,不过相信我,在你没有接触过函数式编程语言之前,你是绝对看不懂此书的,不过好在,我们实际工作中所用到的模板的知识非常有限,本文之前介绍的已然够用了。因此,在继承这块,我不会再讲模板相关的东西。
1. 类的派生
我们写程序时,经常会遇到具有类似属性,但是细节或者行为存在差异的组件。在这种情形下,一种解决方案是将每个组件声明为一个类,并在每个类中实现所有的属性,这将导致大量重复的代码。另一种解决方案是使用继承,从同一个基类派生出类似的类,在基类中实现所有通用的功能,并在派生类中覆盖基本的功能,以实现让每个类都独一无二的行为。
C++派生语法如下:
// 基类
class Base {
// Base class members
};
// 派生类
class Derived: public Base {
// derived class members
};
2. protected 关键字
1. protected 仅限于本类和派生类可以访问。
2. 经过 public 继承,父类中的 private、protected、public 在子类中的访问权限为:不可访问的、protected、public。
3. 继承体系下的函数调用
通过子类对象去调用函数,遵循以下规则:
a) 父类中的非 private 函数,可以由子类去调用。
b) 子类额外编写的函数,也可以正常调用。
c) 子类中含有和父类同名的函数,无论参数列表是否相同,调用的始终是子类的版本。(如果想执行父类的版本,必须显式指定父类的名称)
注意:
父类和子类含有同名函数,那么通过子类对象调用函数,总是调用的子类的版本。这叫做子类的函数隐藏(hidden)了父类的函数。只有显式指定父类类名,才可以调用被隐藏的函数。
只要我们在派生类中写了一个函数,和基类的函数重名(无论参数表是否相同),那么通过派生类对象调用的总是派生类重写的函数。
4. 继承时的对象布局
派生类内部含有一个无名的基类对象,之后才是派生类自己的成员,所以构造派生类时会先构造基类。
1. 子类对象中含有一个父类的无名对象。
2. 构造子类对象时,首先需要调用父类的构造函数,其次是子类的构造函数,析构的顺序与之相反。
3. 子类的对象可以赋值给父类的对象,其中子类对象多余的部分被切除,这叫做对象的切除问题。但是,父类对象赋值给子类对象是非法的。
小结
1. 派生类的构造顺序:
a) 构建基类对象(执行基类对象构造函数)
b) 构造成员对象(执行成员对象构造函数)
c) 执行派生类构造函数函数体
实际上以上三个部分,也都是属于派生类构造函数的,a 和 b 实际上应该在派生类构造函数的初始化列表中完成,如果没有在其初始化列表中显式初始化,则会执行默认初始化。
示例:
// 此处,Student类 public 继承 Person 类 Student(int id, const string &name, int age, const string &school) :Person(id, name, age), school_(school)
{ }
2. 派生类的析构顺序
与派生类的构造顺序相反,如下:
a) 执行派生类析构函数函数体
b) 销毁成员对象(执行成员对象的析构函数)
c)销毁基类对象(执行基类对象的析构函数)
5. 继承与复制控制
这里涉及到两个函数,拷贝构造函数和赋值运算符。如果自己实现这两者,都必须显式调用基类的版本。示例代码如下:
// 此处,Student类 public 继承 Person 类 Student(const Student &s) :Person(s), school_(s.school_) { } Student &operator=(const Student &s) { if(this != &s) { //先对基类对象赋值 //再对自身变量赋值 Person::operator=(s); school_ = s.school_; } return *this; }
总而言之:子类在构造对象时通过初始化列表,指定如何初始化父类的无名对象。而拷贝构造函数用子类去初始化父类对象,赋值运算符中则是显式调用父类的赋值运算符。
6. 禁止复制
之前讲复制控制时,我们已经提过禁止一个类复制的做法是将其拷贝构造函数和赋值运算符设为私有,而且只有声明,没有实现(你们是否还记得谷歌开源风格的那个写法)。
如果我们这里有10个类都需要禁止复制,那么可以每个类都进行上面的操作,但这样导致大量的重复代码,好的解决方案是采用继承,如下:
class NonCopyable { public: NonCopyable() {} ~NonCopyable() {} private: NonCopyable(const NonCopyable &); void operator=(const NonCopyable &); }; // private 可以省略,默认就是 private 继承 class Test : private NonCopyable { };
这样凡是继承了 NonCopyable 的类均失去了复制和赋值的能力。
注意:NonCopyable 要采用私有继承。
7. 总结
1. OOP的第二个性质称为继承。
2. public继承,塑造的是一种“is-a”的关系(子类是父类)。在继承体系中,从上到下是一种具体化的过程,而从下到上则是抽象、泛化的过程。
3.一个类包含另一个类,叫做“has-a”关系,也称为类的组合。
OOP之动态绑定
这是这篇文章的最后一个部分。当我们讲面向对象编程的时候,常会提到其第三个特征为动态绑定。事实上,动态绑定属于运行期多态。前面我们讲过的函数重载属于编译期多态。
这里必须注意,传统的说法,OOP的三大特征封装、继承、多态中的多态仅包含运行期多态。编译期多态并不是面向对象编程特征的一部分。
1. 大前提
基类的指针或者引用指向派生类对象。
2. 静态绑定
静态绑定:编译器在编译期间根据函数的名字和参数,决定调用哪一份代码,这叫做静态绑定,或者早绑定。
在静态绑定期间,通过通过基类指针调用函数,有以下几种情况:
a) 基类中存在的函数,可以调用
b) 子类额外添加的函数,不可以
c) 父子类同名的函数,调用的是父类的版本。
也就是说静态绑定期间,通过基类指针只能调用基类自身的函数。
以上的原因在于:通过基类指针调用函数,编译器把基类指针指向的对象视为基类对象。(实际上更深层次的原因就是因为派生类中含有基类的一个无名对象,这样将派生类的指针赋值给基类指针,实际上基类指针指向的恰好就是派生类中基类的那个对象(基类类型的字节数))
注意:派生类指针可以转化为基类指针,这叫做“向上塑形”,这是绝对安全的,因为继承体系保证了“is-a”的关系,然而,基类指针转化为派生类指针则需要强制转化,而且需要人为的保证安全性,“向下塑形”本质上是不安全的。(读者可以自己想想怎么从内存上来解释)
3. 动态绑定
动态绑定:编译器在编译期间不确定具体的函数调用,而是把这一时机推迟到运行期间,叫做动态绑定,或者晚绑定。
1. C++中触发动态绑定的条件:
a) virtual虚函数
b) 基类的指针或者引用指向了派生类的对象
2. 触发多态绑定后,virtual 数的调用不再是编译期间确定,而是到了运行期,根据基类指针指向的对象的实际类型,来确定调用哪一个函数。
以ps->print();为例:
a) 静态绑定,根据的是 ps 指针本身的类型 (基类 *ps = &派生类对象,基类* 就是 ps 本身的类型)
b) 动态绑定,根据的是 ps 指向实际对象的真实类型。(派生类是 ps 实际指向的类型)
3. 动态绑定的执行流程:
运行期间,因为触发了动态绑定,所以先去寻找对象的vptr(虚指针),根据 vptr 找到虚函数表(vtable),里面存储着虚函数的代码地址,根据 vtable 找到要执行的函数。(注意派生类会从基类继承 vptr)
虚函数具有继承性,如果子类的同名函数,名字和参数与父类的虚函数相同,且返回值相互兼容,那么子类中的该函数也是虚函数。
子类在继承父类虚函数的时候,如果对函数体进行了改写,那么子类的虚函数版本会在 vtable 中覆盖掉父类的版本,这叫做函数的覆盖。
4. 函数的重载、隐藏和覆盖
三者必定是针对同名函数。
1. 重载
构成函数重载的要素有:函数形参表以及成员函数的const属性。
2. 隐藏
实际上,凡是不符合函数覆盖的情形,都属于函数隐藏。
每个类都保持着自己的作用域,在该作用域中定义了成员的名字。在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。(派生类作用域位于基类作用域之内)
以下情形属于隐藏:
i. 父类中的非虚函数,子类名字参数与其一致
ii. 父类中的非虚函数,子类对其参数或返回值做了改动
iii. 父类中的虚函数,但是子类中对其参数做了改动,或者返回值不兼容。
总结一下就以下两点:
a)对于基类的非虚函数,派生类中只要有同名的函数,就属于隐藏。
b)对于基类的虚函数,派生类中有同名函数,且该函数参数类型与基类不一致,或者返回值不兼容,就属于隐藏。
3. 覆盖
覆盖必定是触发多态的情形。
父类中的虚函数,子类的名字、参数与其完全相同,返回值兼容。
5. 注意
1. 不要改动从父类继承而来的非 virtual 函数(即不要触发函数的隐藏)。why?因为这样做根本没有意义。
2. 如果父类中的某函数为虚函数,那么有以下两个选择:
a) 不做任何改动,采用其默认实现。
b) 覆盖父类的实现,提供自己的行为。
3. virtual void run() = 0; 声明了一个纯虚函数,此函数只有声明,没有实现。包含了纯虚函数的类,成为了抽象类。子类在继承抽象类后,必须将其中所有的纯虚函数全部实现,否则仍然是一个抽象类。
4. 在继承体系中,应该把基类的析构函数设为virtual。
5. 动态绑定是运行期的多态,是面向对象的第三个特征,也成为动多态。静多态一般指的是函数重载与模板,但是,这个多态特性不属于面向对象的特性。
(全文完)