每一个成员函数都有一个额外的隐含的形参,这个参数就是this指针,它指向调用对象的地址。默认状况下,this的类型是指向类类型很是量版本的常量指针。能够表示成以下伪代码形式:html
/* 假设如今有一个类Sales_data,以及其很是量Sales_data类型对象,则该隐式的this指针能够写成以下伪代码形式 */ Sales_data *const this = &total;
this指针通常用于解决重名问题和返回自身的值或者引用。例如: ios
struct A{ int a; void test(int a){ this->a = a; } };
test函数的形参a和类成员a成名,根据就近原则,直接使用a,调用的是形参a,那么如何使用被屏蔽的成员a呢,这里就是采用this指针。 c++
紧随参数列表以后的const关键字做用为:修改隐式this指针所指向的对象的类型,以下:git
/* 假设如今有一个类Sales_data,以及Sales_data类型对象,则在const成员函数中隐式的this指针能够写成以下伪代码形式 */ const Sales_data *const this = &total;
这里加const的含义是,这个函数不能修改本对象,其实就是函数体内不得对类的成员进行修改。const主要起到保护的做用。 程序员
注意如下几点: github
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,怎么完成初始化工做?!
到此为止,构成函数重载的要素有:类的名称、函数名、函数形参表以及成员函数的const属性。事实上,函数签名就是由这几个部分构成。
在这里咱们解释一个问题: 为何C语言里面没有函数重载? 由于在编译器编译C程序时会维护一张符号表,C语言在记载函数的时候就是简单的记录函数的名字,因此函数名就是C函数的惟一标识。当咱们试图定义两个名字相同的函数时,就发生了重定义。
C++是怎么作的呢? 很显然,对于普通函数,它的符号(惟一标识)是根据函数名和参数列表生成的,对于类的成员函数,还要加上类名和const属性,因此咱们进行函数重载的时候,这些函数在符号表中的标识是不相同的。 C++正是经过这种机制实现了函数的重载。
注意:C++编译器生成函数符号的时候没有考虑返回值,这也是函数重载和返回值无关的缘由。
构造函数有一个特殊的地方,就是它能够包含一个构造函数初始化列表,以下:
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类类型对象必须有显示定义的默认构造函数才能够执行默认初始化)
考虑下面的类:
class X { int i; int j; public: X(int val) : j(val), i(j) { } };
咱们的设想是这样的,用val初始化j,用j的值初始化i,然而这里初始化的次序是先i而后j。
记住:类成员初始化的顺序是它们在类中声明的顺序,而不是初始化列表中列出的顺序!
与构造函数同样,析构函数也是一种特殊的函数。构造函数在对象被建立时调用,析构函数则是在对象被销毁时被调用。构造函数与构造函数同样,一样没有返回值,而且析构函数没有任何参数。以下:
~Person(){
}
须要引发注意的是:
a)对于类类型对象foo的析构函数只是在它生命期的最后一刻的回调罢了,管不了foo本身所占的内存,就像本身无法给本身收尸同样。
b)对于堆上的类类型对象:free 干的事情是释放内存。delete 干的事情是调用析构函数,而后释放内存,注意是delete释放的内存空间,而不是析构函数释放的。对于栈上的类类型对象,退出做用域时会自动调用析构函数,而后释放内存。
总结:对于栈上的类类型对象其实和内置类型变量同样,退出做用域后都是由系统自动释放内存的。实际上不管是栈空间,仍是堆空间,内置类型对象和类类型对象销毁时的区别,在于类对象会在销毁前调用析构函数。
不用于普通的数据成员,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 保留字,该保留字只出如今类定义体内部的声明处便可。
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.
多么痛的领悟。
顺序容器主要是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; }
注意,凡是传入迭代器做为指定范围的参数,可使用指针代替。
凡是放入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; }
有四个特殊的迭代器:
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 */
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 头插
1. 删第一个或最后一个元素
相似与插入元素,pop_front或者pop_back能够删除第一个或者最后一个元素
2. 删除容器的一个元素
与insert对应,删除采用的是erase操做,该操做有两个版本:删除由一个迭代器指向的元素,或者删除由一对迭代器标记的一段元素。删除元素须要接收返回值,防止迭代器失效,最好使用while循环。
vector与容量有关的函数:
a)size 元素数目,相似于会议室中人的数目
b)resize 调整元素数目,相似于调整函数
c)capacity 可容纳数目,相似于会议室中的座位数量
d)reserve 告诉vector容器应该预留多少个元素的存储空间
任何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; }
遍历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 */
a) vector采用数组实现,list采用链表。
b) vector支持随机访问,list不提供下标。
c) 大量增长删除的操做适合使用list。
例子以下:
#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; }
注意,迭代器通常是取基地址到尾后地址的一段范围。而下标操做,一般是基地址+长度。
#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 */
#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 */
例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 */
反向迭代器逻辑上指向的元素是物理上指向元素的下一个元素。
在实际(物理)实现上,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 */
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)); }
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: 不存在 */
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 */
map 中key 的值是惟一的,set 中的元素都是惟一的。
1. map和set比较:
a) 两者均使用红黑树实现
b) key须要支持<操做
c) map侧重于key-value的快速查找
d) set侧重于查看元素是否存在
2. 用户没法对map和set中的元素进行排序,否则会干扰map和set自己的实现。
最后注意:map 的 key 值是不可更改的。而 set 中的 value 虽然能够更改,但不建议这样作,真有需求,直接删除便可。
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 */
如下只是对小部分经常使用算法进行了介绍,但具体用法大同小异。
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 */
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. */
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) << '\n'; std::cout << "The largest element is " << *std::max_element(myints,myints+7) << '\n'; // using function myfn as comp: std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myfn) << '\n'; std::cout << "The largest element is " << *std::max_element(myints,myints+7,myfn) << '\n'; // using object myobj as comp: std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myobj) << '\n'; std::cout << "The largest element is " << *std::max_element(myints,myints+7,myobj) << '\n'; 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 */
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 */
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 */
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 */
a)根据一个类去显式或者隐式初始化一个对象
b)复制一个对象,将它做为实参传给一个函数
c)从函数返回时复制一个对象
那么如何完成对象复制的工做?这里须要的就是拷贝构造函数。
只有单个形参,并且该形参是本类类型对象的引用(经常使用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 */
如今来思考一个问题,既然编译器生成的拷贝构造函数工做正常,那么何时须要咱们本身来编写拷贝构造函数呢?这就是下面的深拷贝和浅拷贝的问题。
咱们经过本身定义的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
注意:编译器默认的是浅拷贝,此时若是须要深拷贝,须要本身编写拷贝构造函数。
前面的复制构造函数说的是对象的复制,对象的赋值调用的则是对象的赋值运算符。
对于咱们自定义的类(例如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; }
注意:赋值操做符,须要先释放掉之前持有的资源,同时必须处理自赋值的问题。
若是想禁止复制一个类,应该怎么办?
显然须要把类的复制构造函数设为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 容器中。能够考虑使用智能指针。
1. 复制构造函数、赋值运算符以及析构函数,称为三法则,一旦提供了其中一个,务必提供其他两个。以咱们以前自定义的string类为例:
a) 涉及到深拷贝、浅拷贝问题,因此须要提供拷贝构造函数
b) 而后,为了保持一致,赋值运算符也应该实现深拷贝
c) 既然实现深拷贝,那么一定申请了资源(例如内存),因此必然须要析构函数来手工释放。
2. 一个空类,编译器提供默认无参数构造函数、拷贝构造函数、赋值运算符以及析构函数,一共四个函数(针对03标准,c++11中还有移动构造函数和移动赋值运算符)。
3. 对于复制和赋值,请务必保证在程序的语义上具备一致性。
4. 若是一个类,实现了像value同样的复制和赋值能力(意味着复制和赋值后,两个对象没有任何关联,或者逻辑上看起来无任何关联),那么就称这个类的对象为值语义(value semantics)。若是类不能复制,或者复制后对象之间的资源归属纠缠不清,那么称为对象语义(object semantics),或者引用语义(reference semantics)。
运算符的重载有成员函数和友元两种形式。有的运算符能够选择任意一种实现,有的则必须使用友元函数的形式。
一员运算符有一个参数,二元运算符有两个。对于二元运算符来讲,左侧运算对象传递给第一个参数,而右侧对象传递给第二个参数。若是一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,所以,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。
下面的准则有助于咱们在将运算符定义为成员函数仍是右元函数作出抉择:
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。
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;
综上:重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
这里经过String类的编写,讲述运算符的重载的实际运用。此处代码过长,请读者去个人github上阅读,地址为https://github.com/jianxinzhou/classHub/tree/master/string。
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
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
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
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
除了定义数据成员或函数成员以外,类还能够定义类型成员。例如,标准库的容器类定义了不一样的类型,如 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; }
1. 对于非引用类型的参数,在实参演绎的过程当中,会出现从数组到指针的类型转换,也称为衰退(decay)。
2. C中只有传值,因此C语言中把数组当作函数的参数,老是引起decay问题,丢失数组的长度信息。
3. 引用类型不会引起衰退(decay)。
4. 具体代码参看:
https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E8%A1%B0%E9%80%80
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); //释放内存 }
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函数
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类型以及值初始化。
之因此把继承放在模板以后来讲,是由于模板与泛型编程这块博大精深,真正要精通的话,还得去看 C++ Templates 这本经典的传世之做,不过相信我,在你没有接触过函数式编程语言以前,你是绝对看不懂此书的,不过好在,咱们实际工做中所用到的模板的知识很是有限,本文以前介绍的已然够用了。所以,在继承这块,我不会再讲模板相关的东西。
咱们写程序时,常常会遇到具备相似属性,可是细节或者行为存在差别的组件。在这种情形下,一种解决方案是将每一个组件声明为一个类,并在每一个类中实现全部的属性,这将致使大量重复的代码。另外一种解决方案是使用继承,从同一个基类派生出相似的类,在基类中实现全部通用的功能,并在派生类中覆盖基本的功能,以实现让每一个类都独一无二的行为。
C++派生语法以下:
// 基类
class Base {
// Base class members
};
// 派生类
class Derived: public Base {
// derived class members
};
1. protected 仅限于本类和派生类能够访问。
2. 通过 public 继承,父类中的 private、protected、public 在子类中的访问权限为:不可访问的、protected、public。
经过子类对象去调用函数,遵循如下规则:
a) 父类中的非 private 函数,能够由子类去调用。
b) 子类额外编写的函数,也能够正常调用。
c) 子类中含有和父类同名的函数,不管参数列表是否相同,调用的始终是子类的版本。(若是想执行父类的版本,必须显式指定父类的名称)
注意:
父类和子类含有同名函数,那么经过子类对象调用函数,老是调用的子类的版本。这叫作子类的函数隐藏(hidden)了父类的函数。只有显式指定父类类名,才能够调用被隐藏的函数。
只要咱们在派生类中写了一个函数,和基类的函数重名(不管参数表是否相同),那么经过派生类对象调用的老是派生类重写的函数。
派生类内部含有一个无名的基类对象,以后才是派生类本身的成员,因此构造派生类时会先构造基类。
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)销毁基类对象(执行基类对象的析构函数)
这里涉及到两个函数,拷贝构造函数和赋值运算符。若是本身实现这二者,都必须显式调用基类的版本。示例代码以下:
// 此处,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; }
总而言之:子类在构造对象时经过初始化列表,指定如何初始化父类的无名对象。而拷贝构造函数用子类去初始化父类对象,赋值运算符中则是显式调用父类的赋值运算符。
以前讲复制控制时,咱们已经提过禁止一个类复制的作法是将其拷贝构造函数和赋值运算符设为私有,并且只有声明,没有实现(大家是否还记得谷歌开源风格的那个写法)。
若是咱们这里有10个类都须要禁止复制,那么能够每一个类都进行上面的操做,但这样致使大量的重复代码,好的解决方案是采用继承,以下:
class NonCopyable { public: NonCopyable() {} ~NonCopyable() {} private: NonCopyable(const NonCopyable &); void operator=(const NonCopyable &); }; // private 能够省略,默认就是 private 继承 class Test : private NonCopyable { };
这样凡是继承了 NonCopyable 的类均失去了复制和赋值的能力。
注意:NonCopyable 要采用私有继承。
1. OOP的第二个性质称为继承。
2. public继承,塑造的是一种“is-a”的关系(子类是父类)。在继承体系中,从上到下是一种具体化的过程,而从下到上则是抽象、泛化的过程。
3.一个类包含另外一个类,叫作“has-a”关系,也称为类的组合。
这是这篇文章的最后一个部分。当咱们讲面向对象编程的时候,常会提到其第三个特征为动态绑定。事实上,动态绑定属于运行期多态。前面咱们讲过的函数重载属于编译期多态。
这里必须注意,传统的说法,OOP的三大特征封装、继承、多态中的多态仅包含运行期多态。编译期多态并非面向对象编程特征的一部分。
基类的指针或者引用指向派生类对象。
静态绑定:编译器在编译期间根据函数的名字和参数,决定调用哪一份代码,这叫作静态绑定,或者早绑定。
在静态绑按期间,经过经过基类指针调用函数,有如下几种状况:
a) 基类中存在的函数,能够调用
b) 子类额外添加的函数,不能够
c) 父子类同名的函数,调用的是父类的版本。
也就是说静态绑按期间,经过基类指针只能调用基类自身的函数。
以上的缘由在于:经过基类指针调用函数,编译器把基类指针指向的对象视为基类对象。(实际上更深层次的缘由就是由于派生类中含有基类的一个无名对象,这样将派生类的指针赋值给基类指针,实际上基类指针指向的刚好就是派生类中基类的那个对象(基类类型的字节数))
注意:派生类指针能够转化为基类指针,这叫作“向上塑形”,这是绝对安全的,由于继承体系保证了“is-a”的关系,然而,基类指针转化为派生类指针则须要强制转化,并且须要人为的保证安全性,“向下塑形”本质上是不安全的。(读者能够本身想一想怎么从内存上来解释)
动态绑定:编译器在编译期间不肯定具体的函数调用,而是把这一时机推迟到运行期间,叫作动态绑定,或者晚绑定。
1. C++中触发动态绑定的条件:
a) virtual虚函数
b) 基类的指针或者引用指向了派生类的对象
2. 触发多态绑定后,virtual 数的调用再也不是编译期间肯定,而是到了运行期,根据基类指针指向的对象的实际类型,来肯定调用哪个函数。
以ps->print();为例:
a) 静态绑定,根据的是 ps 指针自己的类型 (基类 *ps = &派生类对象,基类* 就是 ps 自己的类型)
b) 动态绑定,根据的是 ps 指向实际对象的真实类型。(派生类是 ps 实际指向的类型)
3. 动态绑定的执行流程:
运行期间,由于触发了动态绑定,因此先去寻找对象的vptr(虚指针),根据 vptr 找到虚函数表(vtable),里面存储着虚函数的代码地址,根据 vtable 找到要执行的函数。(注意派生类会从基类继承 vptr)
虚函数具备继承性,若是子类的同名函数,名字和参数与父类的虚函数相同,且返回值相互兼容,那么子类中的该函数也是虚函数。
子类在继承父类虚函数的时候,若是对函数体进行了改写,那么子类的虚函数版本会在 vtable 中覆盖掉父类的版本,这叫作函数的覆盖。
三者一定是针对同名函数。
1. 重载
构成函数重载的要素有:函数形参表以及成员函数的const属性。
2. 隐藏
实际上,凡是不符合函数覆盖的情形,都属于函数隐藏。
每一个类都保持着本身的做用域,在该做用域中定义了成员的名字。在继承状况下,派生类的做用域嵌套在基类做用域中。若是不能在派生类做用域中肯定名字,就在外围基类做用域中查找该名字的定义。(派生类做用域位于基类做用域以内)
如下情形属于隐藏:
i. 父类中的非虚函数,子类名字参数与其一致
ii. 父类中的非虚函数,子类对其参数或返回值作了改动
iii. 父类中的虚函数,可是子类中对其参数作了改动,或者返回值不兼容。
总结一下就如下两点:
a)对于基类的非虚函数,派生类中只要有同名的函数,就属于隐藏。
b)对于基类的虚函数,派生类中有同名函数,且该函数参数类型与基类不一致,或者返回值不兼容,就属于隐藏。
3. 覆盖
覆盖一定是触发多态的情形。
父类中的虚函数,子类的名字、参数与其彻底相同,返回值兼容。
1. 不要改动从父类继承而来的非 virtual 函数(即不要触发函数的隐藏)。why?由于这样作根本没有意义。
2. 若是父类中的某函数为虚函数,那么有如下两个选择:
a) 不作任何改动,采用其默认实现。
b) 覆盖父类的实现,提供本身的行为。
3. virtual void run() = 0; 声明了一个纯虚函数,此函数只有声明,没有实现。包含了纯虚函数的类,成为了抽象类。子类在继承抽象类后,必须将其中全部的纯虚函数所有实现,不然仍然是一个抽象类。
4. 在继承体系中,应该把基类的析构函数设为virtual。
5. 动态绑定是运行期的多态,是面向对象的第三个特征,也成为动多态。静多态通常指的是函数重载与模板,可是,这个多态特性不属于面向对象的特性。
(全文完)