基础
初始
参考资料
1. C++的初衷
- 兼容C,优化C。
- C++最大的竞争对手正是C
- C++的目标是:
- 在保证效率与C语言相当的情况下,加强程序的组织性;
- 能保证同样功能的程序,C++更短小
- 不是过渡设计的OO
- C++和C的性能相差只有5%
2. C++有多少坑
- C++在解决了很多C语的坑的同时,也因为OO和泛型又引入了一些坑。
- STL是泛型编程的顶级实践!属于是大师级的作品,一般人很难理解
- 如果你一般用用C++的封装,继承,多态,以及namespace,const, refernece, inline, templete, overloap, autoptr,还有一些OO 模式,并不会出现奇怪的问题。
- C++是一门很恐怖的语言,而比它更恐怖的是很多不合格的程序员在使用着它。
3. C++的未来
C++语言发展大概可以分为三个阶段:
- 第一阶段:这一阶段C++语言基本上是传统类型上的面向对象语言
- 第二阶段:由于标准模板库(STL)和后来的Boost等程式库的出现,泛型程式设计占据了越来越多的比重性。
- 第三阶段:以Loki、MPL等程式库为代表的产生式编程和模板元编程的出现,C++出现了发展历史上又一个新的高峰
C++ Primer
1. 内置类型
char, wchar_t
1 | 'a' '2' ',' // char |
string
1 | "Hello World!" // 以空字符结束 |
short, int, long
- 很少用short,容易赋值越界
- 一般用int就够了
- long计算代价远高于int
1 | 20 // 十进制 |
bool
signed, unsigned
float(32), double(64), long double(96或128)
- float精度损失
- 浮点使用double基本不会错,精度代价可以忽略,有些机器double比float还快
- long double的精度通常没必要,还有额外运行代价
1 | 3.1415 // 默认是double |
2. 变量
左值和右值
- 左值可以出现在赋值语句的左、右
- 右值只能在右边
初始化
1 | int a(123); // 直接初始化,效率更高!!!!! |
- 初始化:创建变量病赋予初始值
- 赋值:擦除当前内容并用新值代替
extern 声明
- 声明不是定义,也不分配存储空间。程序中变量可以声明多次
- 只有定义才分配存储空间。程序中变量只能定义一次。
- const 对象默认为文件的局部变量
引用
引用必须用与该引用同类型的对象初始化
1 | int ival = 1024; |
const 引用是只读的
typedef
typedef 可以用来定义类型的同义词:
1 | typedef double wages; // wages is a synonym for double |
typedef 通常被用于以下三种目的:
- 为了隐藏特定类型的实现,强调使用类型的目的。
- 简化复杂的类型定义,使其更易理解。
- 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
枚举成员是常量
枚举成员值可以是不唯一的
1 | // point2d is 2, point2w is 3, point3d is 3, point3w is 4 |
3. 类
用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:
- 默认情况下,struct 的成员为 public
- 而 class 的成员为 private。
头文件用于声明而不是用于定义
预处理器
- 为了避免名字冲突,预处理器变量经常用全大写字母表示。
预防多次包含同一头文件:
1 |
|
4. string类型
函数
1 | isalnum(c) // 如果 c 是字母或数字,则为 True。 |
5. vector 类型
vector 不是一种数据类型,而只是一个类模板
初始化 vector
1 | vector<T> v1; // vector 保存类型为 T 对象。默认构造函数 v1 为空。 |
可以在运行时高效地添加元素。
vector 操作
1 | v.empty() // 如果 v 为空,则返回 true,否则返回 false。 |
下标操作不添加元素
安全的泛型编程
C++ 程序员习惯于优先选用 != 而不是 < 来编写循环判断条件
调用 size 成员函数而不保存它返回的值。所以我们倾向于在每次循环中测试 size 的当前值,而不是在进入循环前,存储 size 值的副本。
像 size 这样的小库函数几乎都定义为内联函数,所以每次循环过程中调用它的运行时代价是比较小的。
1 | vector<int> ivec; // empty vector |
警告:仅能对确知已存在的元素进行下标操作
试图获取不存在的元素必须产生运行时错误。
所谓的”缓冲区溢出”错误就是对不存在的元素进行下标操作的结果。
使用 迭代器(iterator)。
迭代器是一种检查容器内元素并遍历元素的数据类型。
1 | vector<int>::iterator iter = ivec.begin(); |
迭代器类型可使用解引用操作符(dereference operator)(*)来访问迭代器所指向的元素:
1 | *iter = 0; |
const_iterator 的类型,该类型只能用于读取容器内元素,但不能改变其值。
1 | // use const_iterator because we won't change the elements |
6. 指针和引用的比较
第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。
第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。
1 | int ival = 1024, ival2 = 2048; |
7. 指向 const 对象的指针
C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:
1 | const double *cptr; // cptr may point to a double that is const |
8. 使用数组初始化 vector 对象
1 | const size_t arr_size = 6; |
7. C 风格字符串
- 尽可能使用标准库类型 string
- C++ 语言通过(const)char*类型的指针来操纵 C 风格字符串
操纵 C 风格字符串的标准库函数
``cpp
strlen(s) // 返回 s 的长度,不包括字符串结束符 null
strcmp(s1, s2) // 比较两个字符串 s1 和 s2 是否相同。若 s1 与 s2 相等,返回 0;若 s1 大于 s2,返回正数;若 s1 小于 s2,则返回负数
strcat(s1, s2) // 将字符串 s2 连接到 s1 后,并返回 s1
strcpy(s1, s2) // 将 s2 复制给 s1,并返回 s1
strncat(s1, s2,n) // 将 s2 的前 n 个字符连接到 s1 后面,并返回 s1
strncpy(s1, s2, n) // 将 s2 的前 n 个字符复制给 s1,并返回 s1
1 |
|
动态内存的管理容易出错
- 删除(delete)指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。删除动态分配内存失败称为”内存泄漏(memory leak)”。内存泄漏很难发现,一般需等应用程序运行了一段时间后,耗尽了所有内存空间时,内存泄漏才会显露出来。
- 读写已删除的对象。如果删除指针所指向的对象之后,将指针置为 0 值,则比较容易检测出这类错误。
- 对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏。
9. 显式转换
强制类型转换操作符:static_cast、dynamic_cast、const_cast 和 reinterpret_cast
虽然有时候确实需要强制类型转换,但是它们本质上是非常危险的。
每次使用强制转换前,程序员应该仔细考虑是否还有其他不同的方法可以达到同一目的。如果非强制转换不可,则应限制强制转换值的作用域,并且记录所有假定涉及的类型,这样能减少错误发生的机会。
cout << *beg++;
if (beg != end) cout << " "; // no space after last element
}
cout << endl;
}
含有可变形参的函数
在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参
省略符形参有下列两种形式:
// 当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略符对应的实参则暂停类型检查 void foo(parm_list, ...); void foo(...);提醒一点:省略号的优先级别最低,所以在函数解析时,只有当其它所有的函数都无法调用时,编译器才会考虑调用省略号函数的。
void ArgFunc(const char *str, ... ) { va_list ap; int n = 3; char *s = NULL; int d = 0; double f = 0.0; va_start(ap, str); // 注意!这里第二个参数是本函数的第一个形参 s = va_arg(ap, char*); d = va_arg(ap, int); f = va_arg(ap, double); // 浮点最好用double类型,而不要用float类型;否则数据会有问题 va_end(ap); printf("%s is %s %d, %f", str, s, d, f); } void main() { ArgFunc("The answer", "Hello", 345, 788.234); }仅当形参是引用或指针时,形参是否为 const 才有影响。
- 函数的返回值
返回非引用类型
将函数返回值复制给临时对象
返回引用
!!!!!!!!!!!千万不要返回局部对象的引用!!!!!!!!!!
!!!!!!!!!!!千万不要返回指向局部对象的指针!!!!!!!!!!!!!!!
静态局部对象
这种对象一旦被创建,在程序结束前都不会撤销
在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值
内联函数
调用函数都要做很多工作;
调用前要先保存寄存器,并在返回时恢复; 复制实参; 程序还必须转向一个新位置执行。
inline 函数避免函数调用的开销
它在程序中每个调用点上“内联地”展开
// inline version: find longer of two strings inline const string &shorterString(const string &s1, const string &s2) { return s1.size() < s2.size() ? s1 : s2; } // 调用地方 cout << shorterString(s1, s2) << endl; // 在编译时将展开为: cout << (s1.size() < s2.size() ? s1 : s2) << endl;内联机制适用于优化小的、只有几行的而且经常被调用的函数
把 inline 函数放入头文件
指向函数的指针
像其他指针一样,函数指针也指向某个特定的类型。函数类型由其返回类型以及形参表确定,而与函数名无关:
// pf points to function returning bool that takes two const string references bool (*pf)(const string &, const string &); // *pf 两侧的圆括号是必需的 // 这个语句将 pf 声明为指向函数的指针,它所指向的函数带有两个 const string& 类型的形参和 bool 类型的返回值。
函数指针形参
函数的形参可以是指向函数的指针。这种形参可以用以下两种形式编写:
void useBigger(const string &, const string &, bool(const string &, const string &)); void useBigger(const string &, const string &, bool (*)(const string &, const string &));返回指向函数的指针
int (*ff(int))(int*, int);阅读函数指针声明的最佳方法是从声明的名字开始由里而外理解。
使用 typedef 可使该定义更简明易懂:
PF ff(int); // ff returns a pointer to function typedef int (*PF)(int*, int);
- 顺序容器类型 vector list deque
添加元素可能会使迭代器失效
任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。
关系操作符
/* ivec1: 1 3 5 7 9 12 ivec2: 0 2 4 6 8 10 12 ivec3: 1 3 9 ivec4: 1 3 5 7 ivec5: 1 3 5 7 9 12 */ // ivec1 and ivec2 differ at element[0]: ivec1 greater than ivec2 ivec1 < ivec2 // false ivec2 < ivec1 // true // ivec1 and ivec3 differ at element[2]: ivec1 less than ivec3 ivec1 < ivec3 // true // all elements equal, but ivec4 has fewer elements, so ivec1 is greater than ivec4 ivec1 < ivec4 // false ivec1 == ivec5 // true; each element equal and same number of elements ivec1 == ivec4 // false; ivec4 has fewer elements than ivec1 ivec1 != ivec4 // true; ivec4 has fewer elements than ivec1访问元素
c.back() 返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义 c.front() 返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义 c[n] 返回下标为 n 的元素的引用如果 n <0 或 n >= c.size(),则该操作未定义;只适用于 vector 和 deque 容器 c.at(n) 返回下标为 n 的元素的引用。如果下标越界,则该操作未定义;只适用于 vector 和 deque 容器删除元素
c.erase(p) 删除迭代器 p 所指向的元素 返回一个迭代器,它指向被删除元素后面的元素。如果 p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置。如果 p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义 c.erase(b,e) 删除迭代器 b 和 e 所标记的范围内所有的元素 返回一个迭代器,它指向被删除元素段后面的元素。如果 e 本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器的超出末端的下一位置 c.clear() 删除容器 c 内的所有元素。返回 void c.pop_back() 删除容器 c 的最后一个元素。返回 void。如果 c 为空容器,则该函数未定义 c.pop_front() 删除容器 c 的第一个元素。返回 void。如果 c 为空容器,则该函数未定义 只适用于 list 或 deque 容器赋值操作
c1 = c2 删除容器 c1 的所有元素,然后将 c2 的元素复制给 c1。c1 和 c2 的类型(包括容器类型和元素类型)必须相同 c1.swap(c2) 交换内容:调用完该函数后,c1 中存放的是 c2 原来的元素,c2 中存放的则是 c1 原来的元素。c1 和 c2 的类型必须相同。 该函数的执行速度通常要比将 c2 复制到 c1 的操作快 c.assign(b,e) 重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。 b 和 e 必须不是指向 c 中元素的迭代器 c.assign(n,t) 将容器 c 重新设置为存储 n 个值为 t 的元素assign 操作首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中
容器的选用
• 在容器的中间位置添加或删除元素的代价。
• 执行容器元素的随机访问的代价。插入操作如何影响容器的选择
list 容器表示不连续的内存区域,允许向前和向后逐个遍历元素。在任何位置都可高效地 insert 或 erase 一个元素 vector 容器,除了容器尾部外,其他任何位置上的插入(或删除)操作都要求移动被插入(或删除)元素右边所有的元素 deque 容器拥有更加复杂的数据结构。从 deque 队列的两端插入和删除元素都非常快。在容器中间插入或删除付出的代价将更高元素的访问如何影响容器的选择
vector 和 deque 容器都支持对其元素实现高效的随机访问 list 容器的元素之间移动的唯一方法是顺序跟随指针如果无法确定某种应用应该采用哪种容器,则编写代码时尝试只使用 vector 和 lists 容器都提供的操作:使用迭代器,而不是下标,并且避免随机访问元素。这样编写,在必要时,可很方便地将程序从使用 vector 容器修改为使用 list 的容器。
关联容器 map set multimap multiset
map 类定义的类型
map<K,V>::key_type 在 map 容器中,用做索引的键的类型 map<K,V>::mapped_type 在 map 容器中,键所关联的值的类型 map<K,V>::value_type 一个 pair 类型,它的 first 元素具有 const map<K,V>::key_type 类型,而 second 元素则为 map<K,V>::mapped_type 类型
对于 map 容器,如果下标所表示的键在容器中不存在,则添加新元素
查找并读取 map 中的元素
使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素
map<string,int> word_count; int occurs = word_count["foobar"]; // 如果“foobar”不存在,则在 map 中插入具有该键的新元素,其关联的值为 0不修改 map 对象的查询操作
m.count(k) 返回 m 中 k 的出现次数 m.find(k) 如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器从 map 对象中删除元素
m.erase(k) 删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数 m.erase(p) 从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void m.erase(b,e) 从 m 中删除一段范围内的元素,该范围由迭代器对 b 和 e 标记。b 和 e 必须标记 m 中的一段有效范围:即 b 和 e 都必须指向 m中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型map 对象的迭代遍历
map<string, int>::const_iterator map_it = word_count.begin(); // for each element in the map while (map_it != word_count.end()) { // print the element key, value pairs cout << map_it->first << " occurs " << map_it->second << " times" << endl; ++map_it; // increment iterator to denote the next element }set 类型
set 中添加元素
set<string> set1; // empty set set1.insert("the"); // set1 now has one element set1.insert("and"); // set1 now has two elements set<int> iset2; // empty set iset2.insert(ivec.begin(), ivec.end()); // iset2 has 10 elements从 set 中获取元素
set<int>::iterator set_it = iset.find(1); *set_it = 11; // error: keys in a set are read-only cout << *set_it << endl; // ok: can read the keymultimap
由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素
带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数
!!!!在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放
使用 find 和 count 操作
// author we'll look for string search_item("Alain de Botton"); // how many entries are there for this author typedef multimap<string, string>::size_type sz_type; sz_type entries = authors.count(search_item); // get iterator to the first entry for this author multimap<string,string>::iterator iter = authors.find(search_item); for (sz_type cnt = 0; cnt != entries; ++cnt, ++iter) cout << iter->second << endl; // print each title
泛型算法
每个泛型算法的实现都独立于单独的容器。
比起死记全部一百多种算法,了解算法的设计可使我们更容易学习和使用它们。
find 运算 // value we'll look for int search_value = 42; // call find to see if that value is present vector<int>::const_iterator result = find(vec.begin(), vec.end(), search_value); find 必须包含以下步骤: 1. 顺序检查每个元素。 2. 如果当前元素等于要查找的值,那么返回指向该元素的迭代器。 3. 否则,检查下一个元素,重复步骤 2,直到找到这个值,或者检查完所有的元素为止。 4. 如果已经到达集合末尾,而且还未找到该值,则返回某个值,指明要查找的值在这个集合中不存在。 算法的明确要求如下: 1. 需要某种遍历集合的方式:能够从一个元素向前移到下一个元素。 2. 必须能够知道是否到达了集合的末尾。 3. 必须能够对容器中的每一个元素与被查找的元素进行比较。 4. 需要一个类型指出元素在容器中的位置,或者表示找不到该元素。算法永不执行容器提供的操作
只读算法 find accumulate find_first_of
accumulate 将 sum 设置为 vec 的元素之和再加上 42
int sum = accumulate(vec.begin(), vec.end(), 42); // 第三个形参则是累加的初值find_first_of 这个算法带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器。
size_t cnt = 0; list<string>::iterator it = roster1.begin(); // look in roster1 for any name also in roster2 while ((it = find_first_of(it, roster1.end(), roster2.begin(), roster2.end())) != roster1.end()) {}写容器元素的算法
写入输入序列的元素 fill 函数,这个算法只会对输入范围内已存在的元素进行写入操作
fill(vec.begin(), vec.end(), 0); // reset each element to 0 // set subsequence of the range to 10 fill(vec.begin(), vec.begin() + vec.size()/2, 10);对容器元素重新排序的算法
// sort words alphabetically so we can find the duplicates sort(words.begin(), words.end());
- 类
类背后蕴涵的基本思想是 数据抽象和 封装
某种类可能具有某些操作,这些操作应该返回引用,返回 *this
!!!!!!!!流式编程
class Screen { public: // interface member functions Screen& move(index r, index c); Screen& set(char); }; // move cursor to given position, and set that character myScreen.move(4,0).set('#');这个语句等价于:
myScreen.move(4,0); myScreen.set('#');构造函数初始化式
Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0) { }类类型的数据成员的初始化式
// alternative definition for Sales_item default constructor Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}成员初始化的次序
成员被初始化的次序就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。
友元
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。
友元的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。
将友元声明成组地放在类定义的开始或结尾是个好主意
class girl; class boy{ public: void disp(girl &); }; void boy::disp(girl &x) //函数disp()为类boy的成员函数,也是类girl的友元函数{ cout<<"girl's name is:"<<x.name<<",age:"<<x.age<<endl;//借助友元,在boy的成员函数disp中,借助girl的对象,直接访问girl的私有变量 } class girl{ private: char *name; int age; friend boy; //声明类boy是类girl的友元 };友元函数和类的成员函数的区别
成员函数有this指针,而友元函数没有this指针。 友元函数是不能被继承的,就像父亲的朋友未必是儿子的朋友。类的 static 成员
使用 static 成员而不是全局对象有三个优点
1. static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。 2. 可以实施封装。static 成员可以是私有成员,而全局对象不可以。 3. 通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。static 函数没有 this 指针
复制构造函数
是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
A a; A b(a); A b=a; // 都是拷贝构造函数来创建对象b深拷贝与浅拷贝:
浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在) 深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝
析构函数
何时调用析构函数
撤销类对象时会自动调用析构函数: // p points to default constructed object Sales_item *p = new Sales_item; { // new scope Sales_item item(*p); // copy constructor copies *p into item delete p; // 当对象的引用或指针超出作用域时,不会运行析构函数,动态分配的对象只有在指向该对象的指针被删除时才撤销 } // 变量(如 item)在超出作用域时应该自动撤销 { Sales_item *p = new Sales_item[10]; // dynamically allocated vector<Sales_item> vec(p, p + 10); // local object delete [] p; // array is freed; destructor run on each element } // vec goes out of scope; destructor run on each element容器中的元素总是按逆序撤销:首先撤销下标为 size() - 1 的元素,然后是下标为 size() - 2 的元素……直到最后撤销下标为 [0] 的元素
何时编写显式析构函数
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。 * 这个规则常称为 三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。合成析构函数
按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。 并不删除指针成员所指向的对象即使我们编写了自己的析构函数,合成析构函数仍然运行
管理指针成员
1. 指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。 2. 类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。 3. 类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理。***一个带指针成员的简单类
// class that has a pointer member that behaves like a plain pointer class HasPtr { public: // copy of the values we're given HasPtr(int *p, int i): ptr(p), val(i) { } // const members to return the value of the indicated data member int *get_ptr() const { return ptr; } int get_int() const { return val; } // non const members to change the indicated data member void set_ptr(int *p) { ptr = p; } void set_int(int i) { val = i; } // return or change the value pointed to, so ok for const objects int get_ptr_val() const { return *ptr; } void set_ptr_val(int val) const { *ptr = val; } private: int *ptr; int val; }; 默认复制/赋值与指针成员 int obj = 0; HasPtr ptr1(&obj, 42); // int* member points to obj, val is 42 HasPtr ptr2(ptr1); // int* member points to obj, val is 42 类本身无法避免悬垂指针***定义智能指针类
定义智能指针的通用技术是采用一个 引用计数***定义值型类
复制构造函数不再复制指针,它将分配一个新的 int 对象,并初始化该对象以保存与被复制对象相同的值。每个对象都保存属于自己的 int 值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针 class HasPtr { public: // no point to passing a pointer if we're going to copy it anyway // store pointer to a copy of the object we're given HasPtr(const int &p, int i): ptr(new int(p)), val(i) {} // copy members and increment the use count HasPtr(const HasPtr &orig): ptr(new int (*orig.ptr)), val(orig.val) { } // 赋值操作符不需要分配新对象,它只是必须记得给其指针所指向的对象赋新值,而不是给指针本身赋值 HasPtr& operator=(const HasPtr&){ // Note: Every HasPtr is guaranteed to point at an actual int; // We know that ptr cannot be a zero pointer *ptr = *rhs.ptr; // copy the value pointed to val = rhs.val; // copy the int return *this; } ~HasPtr() { delete ptr; } // accessors must change to fetch value from Ptr object int get_ptr_val() const { return *ptr; } int get_int() const { return val; } // change the appropriate data member void set_ptr(int *p) { ptr = p; } void set_int(int i) { val = i; } // return or change the value pointed to, so ok for const objects int *get_ptr() const { return ptr; } void set_ptr_val(int p) const { *ptr = p; } private: int *ptr; // points to an int int val; };
虚函数
定义基类
class Item_base { public: Item_base(const std::string &book = "",double sales_price = 0.0): isbn(book), price(sales_price) { } std::string book() const { return isbn; } virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() { } private: std::string isbn; // identifier for the item protected: double price; // normal, undiscounted price };定义派生类
class Bulk_item : public Item_base { public: double net_price(std::size_t) const; private: std::size_t min_qty; // minimum purchase for discount to apply double discount; // fractional discount to apply };可以在运行时确定 virtual 函数的调用
void print_total(ostream &os, const Item_base &item, size_t n) { // calls Item_base::book os << "ISBN: " << item.book() // virtual call: which version of net_price to call isresolved at run time << item.net_price(n) << endl; }在第一个调用中,item 形参在运行时绑定到 Item_base 类型的对象,因此,print_total 内部调用 Item_base 中定义的 net_price 版本。在第二个调用中,item 形参绑定到 Bulk_item 类型的对象,从 print_total 调用的是Bulk_item 类定义的 net_price 版本
Item_base base; Bulk_item derived; print_total(cout, base, 10); // calls Item_base::net_price print_total(cout, derived, 10); // calls Bulk_item::net_price非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定
所以,无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用 Item_base 中定义的 不管传给 print_total 的实参的实际类型是什么,对 book 的调用在编译时确定为调用 Item_base::book模板和泛型编程
定义函数模板
template <typename T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; } cout << compare(1, 0) << endl; string s1 = "hi", s2 = "world"; cout << compare(s1, s2) << endl;inline 函数模板
函数模板可以用与非模板函数一样的方式声明为 inline
template <typename T> inline T min(const T&, const T&); // 函数模板可以用与非模板函数一样的方式声明为 inline inline template <typename T> T min(const T&, const T&); // error定义类模板
template <class Type> class Queue { public: Queue (); // default constructor Type &front (); // return element from head of Queue const Type &front () const; void push (const Type &); // add element to back of Queue void pop(); // remove element from head of Queue bool empty() const; // true if no elements in the Queue private: // ... }; Queue<int> qi; // Queue that holds ints Queue< vector<double> > qc; // Queue that holds vectors of doubles Queue<string> qs; // Queue that holds strings模板形参
类型形参由关键字 class 或 typename 后接说明符构成,关键字 typename 和 class 具有相同含义
模板形参选择的名字没有本质含义
template <class Glorp> int compare(const Glorp &v1, const Glorp &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }该代码定义的 compare 模板与前面一样
template <class Parm, class U> Parm fcn(Parm* array, U value) { typename Parm::size_type * p; // ok: declares p to be a pointer }非类型模板形参
// initialize elements of an array to zero template <class T, size_t N> void array_init(T (&parm)[N]) { for (size_t i = 0; i != N; ++i) { parm[i] = 0; } } int x[42]; double y[10]; array_init(x); // instantiates array_init(int(&)[42] array_init(y); // instantiates array_init(double(&)[10]类模板形参是必需的
Queue qs; // error: which template instantiation? Queue<int> qi; // ok: defines Queue that holds ints Queue<string> qs; // ok: defines Queue that holds strings
- 异常处理
STL
http://www.cnblogs.com/shiyangxt/archive/2008/09/11/1289493.html
STL简介
STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称。
STL的代码从广义上讲分为三类:
algorithm(算法) container(容器) iterator(迭代器)STL被组织为下面的13个头文件:
<algorithm> <deque> <functional> <iterator> <vector> <list> <map> <memory> <numeric> <queue> <set> <stack> <utility>算法
是所有STL头文件中最大的一个,它是由一大堆模版函数组成的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。 // 返回容器中最小值和最大值。max_element(first,end,cmp);其中cmp为可选择参数! int num[]={2,3,1,6,4,5}; cout<<"最小值是 "<<*min_element(num,num+6)<<endl; cout<<"最大值是 "<<*max_element(num,num+6)<<endl; cout<<"最小值是 "<<*min_element(num,num+6,[](int a,int b){return a <b; })<<endl; cout<<"最大值是 "<<*max_element(num,num+6,[](int a,int b){return a <b; })<<endl; list<int> listTwo; //从前面向listTwo容器中添加数据 listTwo.push_front ('A'); //从后面向listTwo容器中添加数据 listTwo.push_back ('x'); list<int>::iterator j; //使用STL的max_element算法求listTwo中的最大元素并显示 j=max_element(listTwo.begin(),listTwo.end()); cout << "The maximum element in listTwo is: "<<char(*j)<<endl;体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。 //累加求和,头两个形参指定要累加的元素范围,第三个形参则是累加的初值 int sum = accumulate(vec.begin() , vec.end() , 42); struct Grade { string name; int grade; }; Grade subject[3] = { { "English", 80 }, { "Biology", 70 }, { "History", 90 } }; // 第四个形参,是自己动手写一个回调函数来实现自定义数据的处理 int sum = accumulate(subject, subject + 3, 0, [](int a, Grade b){return a + b.grade; }); cout << sum << endl;中则定义了一些模板类,用以声明函数对象。
容器
数据结构本身的重要性不会逊于操作于数据结构的算法的重要性
向量(vector) 连续存储的元素
#include <iostream> #include <string> #include <vector> using namespace std; int main(){ string str="shiyang"; vector <string> vecstr; vecstr.push_back(str); vector <string> ::iterator iter= vecstr.begin(); cout<<*iter<<endl; return 0; }列表(list) 由节点组成的双向链表,每个结点包含着一个元素
typedef list<int> LISTINT; //用LISTINT创建一个名为listOne的list对象 LISTINT listOne; //从前面向listOne容器中添加数据 listOne.push_front (2); //从后面向listOne容器中添加数据 listOne.push_back (3); //声明i为迭代器 LISTINT::iterator i; //从前向后显示listOne中的数据 for (i = listOne.begin(); i != listOne.end(); ++i) cout << *i << " "; //从后向前显示listOne中的数据 LISTINT::reverse_iterator ir; for (ir =listOne.rbegin(); ir!=listOne.rend();ir++) cout << *ir << " ";双队列(deque) 连续存储的指向不同元素的指针所组成的数组
集合(set) 由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序
多重集合(multiset) 允许存在两个次序相等的元素的集合
栈(stack) 后进先出的值的排列
队列(queue) 先进先出的执的排列
优先队列(priority_queue) 元素的次序是由作用于所存储的值对上的某种谓词决定的的一种队列
映射(map) 由{键,值}对组成的集合,以某种作用于键对上的谓词排列
map<int,int*> m_iip; int i=0; m_iip.insert(make_pair(34,&i));多重映射(multimap) 允许键对有相等的次序的映射
迭代器
迭代器在STL中用来将算法和容器联系起来
是一个很小的头文件,它包括了贯穿使用在STL中的几个模板的声明, 中提供了迭代器使用的许多方法 的描述则十分的困难,它以不同寻常的方式为容器中的元素分配存储空间,同时也为某些算法执行期间产生的临时对象提供机制, 中的主要部分是模板类allocator,它负责产生所有容器中的默认分配器。
50条忠告:
1.把C++当成一门新的语言学习; 2.看《Thinking In C++》,不要看《C++变成死相》; 3.看《The C++ Programming Language》和《Inside The C++ Object Model》,不要因为他们很难而我们自己是初学者所以就不看; 4.不要被VC、BCB、BC、MC、TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言; 5.不要放过任何一个看上去很简单的小编程问题——他们往往并不那么简单,或者可以引伸出很多知识点; 6.会用Visual C++,并不说明你会C++; 7.学class并不难,template、STL、generic programming也不过如此——难的是长期坚持实践和不遗余力的博览群书; 8.如果不是天才的话,想学编程就不要想玩游戏——你以为你做到了,其实你的C++水平并没有和你通关的能力一起变高——其实可以时刻记住:学C++是为了编游戏的; 9.看Visual C++的书,是学不了C++语言的; 16.把时髦的技术挂在嘴边,还不如把过时的技术记在心里; 18.学习编程最好的方法之一就是阅读源代码; 19.在任何时刻都不要认为自己手中的书已经足够了; 20.请阅读《The Standard C++ Bible》(中文版:标准C++宝典),掌握C++标准; 21.看得懂的书,请仔细看;看不懂的书,请硬着头皮看; 22.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍; 23.请看《Effective C++》和《More Effective C++》以及《Exceptional C++》; 24.不要停留在集成开发环境的摇篮上,要学会控制集成开发环境,还要学会用命令行方式处理程序; 25.和别人一起讨论有意义的C++知识点,而不是争吵XX行不行或者YY与ZZ哪个好; 26.请看《程序设计实践》,并严格的按照其要求去做; 27.不要因为C和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样; 28.C++绝不是所谓的C的“扩充”——如果C++一开始就起名叫Z语言,你一定不会把C和Z语言联系得那么紧密; 29.请不要认为学过XX语言再改学C++会有什么问题——你只不过又在学一门全新的语言而已; 30.读完了《Inside The C++ Object Model》以后再来认定自己是不是已经学会了C++; 31.学习编程的秘诀是:编程,编程,再编程; 32.请留意下列书籍:《C++面向对象高效编程(C++ Effective Object-Oriented Software Construction)》《面向对象软件构造(Object-Oriented Software Construction)》《设计模式(Design Patterns)》《The Art of Computer Programming》; 34.请把书上的程序例子亲手输入到电脑上实践,即使配套光盘中有源代码; 35.把在书中看到的有意义的例子扩充; 36.请重视C++中的异常处理技术,并将其切实的运用到自己的程序中; 37.经常回顾自己以前写过的程序,并尝试重写,把自己学到的新知识运用进去; 38.不要漏掉书中任何一个练习题——请全部做完并记录下解题思路; 39.C++语言和C++的集成开发环境要同时学习和掌握; 40.既然决定了学C++,就请坚持学下去,因为学习程序设计语言的目的是掌握程序设计技术,而程序设计技术是跨语言的; 41.就让C++语言的各种平台和开发环境去激烈的竞争吧,我们要以学习C++语言本身为主; 42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成以保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43); 43.别心急,设计C++的class确实不容易;自己程序中的class和自己的class设计水平是在不断的编程实践中完善和发展的; 44.决不要因为程序“很小”就不遵循某些你不熟练的规则——好习惯是培养出来的,而不是一次记住的; 45.每学到一个C++难点的时候,尝试着对别人讲解这个知识点并让他理解——你能讲清楚才说明你真的理解了; 46.记录下在和别人交流时发现的自己忽视或不理解的知识点; 47.请不断的对自己写的程序提出更高的要求,哪怕你的程序版本号会变成Version 100.XX; 48.保存好你写过的所有的程序——那是你最好的积累之一; 49.请不要做浮躁的人; 50.请热爱C++!C++头文件一览
传统 C++
#include <assert.h> 设定插入点 #include <ctype.h> 字符处理 #include <errno.h> 定义错误码 #include <float.h> 浮点数处理 #include <fstream.h> 文件输入/输出 #include <iomanip.h> 参数化输入/输出 #include <iostream.h> 数据流输入/输出 #include <limits.h> 定义各种数据类型最值常量 #include <locale.h> 定义本地化函数 #include <math.h> 定义数学函数 #include <stdio.h> 定义输入/输出函数 #include <stdlib.h> 定义杂项函数及内存分配函数 #include <string.h> 字符串处理 #include <strstrea.h> 基于数组的输入/输出 #include <time.h> 定义关于时间的函数 #include <wchar.h> 宽字符处理及输入/输出 #include <wctype.h> 宽字符分类标准 C++
#include <algorithm> 通用算法 #include <bitset> 位集容器 #include <cctype> #include <cerrno> #include <clocale> #include <cmath> #include <complex> 复数类 #include <cstdio> #include <cstdlib> #include <cstring> #include <ctime> #include <deque> 双端队列容器 #include <exception> 异常处理类 #include <fstream> #include <functional> 定义运算函数(代替运算符) #include <limits> #include <list> 线性列表容器 #include <map> 映射容器 #include <iomanip> #include <ios> 基本输入/输出支持 #include <iosfwd> 输入/输出系统使用的前置声明 #include <iostream> #include <istream> 基本输入流 #include <ostream> 基本输出流 #include <queue> 队列容器 #include <set> 集合容器 #include <sstream> 基于字符串的流 #include <stack> 堆栈容器 #include <stdexcept> 标准异常类 #include <streambuf> 底层输入/输出支持 #include <string> 字符串类 #include <utility> 通用模板类 #include <vector> 动态数组容器 #include <cwchar> #include <cwctype>C99 增加
#include <complex.h> 复数处理 #include <fenv.h> 浮点环境 #include <inttypes.h> 整数格式转换 #include <stdbool.h> 布尔环境 #include <stdint.h> 整型环境 #include <tgmath.h> 通用类型数学宏
Effective C++ 笔记
术语
- 声明:告诉编译器某个东西的名称和类型
下面都是声明:
extern int x; //对象声明 std::size_t numDigits(int number); //函数声明 class Widget; //类声明 template<typename T> //模板声明- 定义:提供编译器一些声明所遗漏的细节
对于对象,定义是编译器为此对象拨发内存的地点;对于function,定义是提供代码本体;对于class,定义列出他们的成员
int x; //对象的定义 std::size_t numDigite(int number) //函数定义 { std::size_t digitsSoFar = 1; while ((number /= 10) != 0) ++digitsSoFar; return digitsSoFar; } class Widget { //class定义 public: Widget(); ~Widget(); ... }; template<tempname T> //模板定义- 初始化:给对象初值
默认构造函数要不没有参数,要不每个参数都要有缺省值;
class A { public: A(); //默认构造函数 explicit A(int a = 0, bool b = true); // 默认构造函数 explicit A(int x); //不是默认构造函数 } // explicit用来阻止隐式类型转换 //拷贝构造函数用来以同型对象初始化自我对象 class B { public: B(); B(const B& rhs); //拷贝构造函数 B& operator=(const B& rhs); //拷贝赋值操作符 bool hasAQ(B b); //b是以值传递的,会调用拷贝构造函数复制到b体内 }; B b1; //调用默认构造函数 B b2(b1); //调用拷贝构造函数 b1 = b2; //调用拷贝赋值操作符 B b3 = b2; //调用拷贝构造函数- 不明确行为
无法预估运行时会发生什么
int* p = 0; //p是个null指针 std::cout << *p; //对null指针取值会导致不明行为 char name[] = "Darla"; //name是个大小为6的数组,别忘了最尾端的null!!! char c = name[10]; //指向一个无效的数组索引,导致不明行为尽量用const,enum,inline替换#define
对于常量,最好用const或enums替换#define
对于函数,最好改用inline函数替换#define
// 取a和b较大值 #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) int a = 5, b = 0; CALL_WITH_MAX(++a,b); //a 被累加两次 // 替换成 template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }尽可能使用const
声明const 可帮助编译器检测出错误用法
char g[] = "Hello"; const char* p = g; // 指针可变,数据不可变 char* const p = g; // 指针不可变,数据可变 const char* const p = g; // 指针数据都不可变确定对象使用前已被初始化
内置对象要手工初始化
构造函数最好使用成员初值列,排列顺序应和在class中声明次序相同
A::A() :name(), address(), numTimes(0) {}为避免跨编译单元初始化次序问题,以local static对象替换non-local static对象
C++默认编写并调用了哪些函数,如不想使用编译器自动生成的函数,就要明确拒绝
编译器会默认为class创建默认构造函数、析构函数、拷贝构造函数、拷贝赋值操作符
明确拒绝自动生成的函数,可声明为private并不要实现。
如果写下
class Empty {};相当于:
class Empty { public: Empty(){...} Empty(const Empty& rhs){...} ~Empty(){...} Empty& operator=(const Empty& rhs){...} };为多态基类声明virtual析构函数
多态性质的父类应该拥有一个virtual析构函数
如果class拥有任何virtual函数,就应该拥有一个virtual析构函数
如果class不是作为基类或不是为了多态,就不要声明virtual析构函数
class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); // virtual析构函数,可以销毁包括子类的整个对象 } class AtomicClock: public TimeKeeper {}; //原子表 class WaterClock: public TimeKeeper {}; //水表工厂函数
TimeKeeper* getTimeKeeper(); //返回指向TimeKeeper派生类对象的指针使用
TimeKeeper* ptk = getTimeKeeper(); ... // 使用ptk delete ptk; //释放局部销毁问题
当子类对象经由父类指针被delete,如果父类没有virtual析构函数,则会只执行父类析构,不执行子类析构,造成局部销毁问题别让异常逃离析构函数
析构函数绝对不要吐出异常,应该捕捉任何异常,然后吞下他们或结束程序
绝不在构造和析构过程中调用virtual函数
由于基类构造函数的执行早于子类构造函数,在基类的构造期间,对象类型是基类,而不是子类
另 operator= 返回一个 *this的引用
为了实现连锁赋值,赋值操作符必须返回一个引用指向操作符左侧实参
x = y = z = 15;// 等效于x = (y = (z = 15)); Widget& operator=(const Widget& rhs) { ... return* this; }在operator=中处理“自我赋值”
判断是否相同
W& W::operator=(const W& rhs) { if(this == &rhs) return *this; // 如果是自我赋值,则直接返回 delete pb; pb = new Bitmap(*rhs.pb); return *this; }复制对象时勿忘每一个成分
只留两个函数负责对象拷贝:
拷贝构造函数 拷贝赋值操作符自己写拷贝函数,一旦发生继承,潜藏危机四伏!必须小心地复制父类的属性。
不要以某个拷贝函数实现另一个拷贝函数,应将共同机能放进第三个函数,并由两个拷贝函数共同调用
以对象管理资源
获得资源后立即放入管理对象中。std::auto_ptr或std::tr1::shared_ptr,后者是较佳选择,因为其拷贝行为比较直观。auto_ptr的复制动作会使它指向null。
管理对象运用析构函数确保资源被释放。
资源管理类中小心拷贝行为
在资源管理类中提供对原始资源的访问
对原始资源的访问一般进行显式转换比较安全
std::tr1::shared_ptr<B> pInv(createB()); // pInv为资源管理对象 pInv.get(); // 为显示转换成对象B隐式转换对客户比较方便
成对使用new和delete要采取相同形式
new 使用 [],delete 也使用 []
独立语句将新建对象放入智能指针
否则,一旦异常抛出,则导致难以察觉的资源泄露
让接口易被正确使用,不易误用
接口一致,建立新类型,束缚对象值
设计class如同设计type
- 如何创建、销毁
- 初始化和赋值的差别
- 如果被值传递,拷贝构造函数如何实现
- 合法值是什么,必须进行的错误检查工作
- 继承关系如何,是否受到virtual影响,特别是析构函数
- 需要什么样的转换,类型转换操作符或函数的构造
- private函数的设置
- 哪个成员是public、protected、private
- 未声明接口是什么,一般来说指那些在新type内部工作的接口或类型。它对效率、异常安全性及资源运用提供保证。
pass by referrence to const 替换 pass by value
相对于传“值”,一个更好的替代方法是传“const引用”
对于内置类型(bulit-in type)对象以及STL中的迭代器、函数对象,Scott还是建议使用传值方式传递,原因是他们本来就是被设计成适合传值传递的
必须返回对象时,别返回其引用
因为引用的很可能是局部变量,返回引用后,局部变量会自动销毁,引用指向了一般并不存在的对象了
可以返回堆对象的引用,或局部static对象的引用
把成员变量声明为private
这样做可以保证客户访问数据的一致性,做到细微的访问控制,启用约束条件,提供弹性
用 non-member、non-friend 替换 member 函数
这样可以增加封装性,弹性和扩展性
// code in class_a.h namespace AllAboutClassA { class ClassA { // ..}; // .. } // code in utility_1.h // .. namespace AllAboutClassA { void WrapperFunction_1() { // ..}; // .. } // .. // code in utility_2.h // .. namespace AllAboutClassA { void WrapperFunction_2() { // ..}; // .. }如果有需要添加新的非成员函数,我们要做的只是在相同的名字空间中定义这些函数就可以,那个类丝毫不会被影响,也即所谓的易扩展性吧。
对于类的用户来说,这样的实现方式(指用非成员函数)自由度更大
如果你需要为某函数的所有参数进行类型转换,那这个函数必须是non-member
考虑写出不抛异常的swap函数
将两个对象的值彼此赋予对方
当std::swap效率不高时,提供一个swap成员函数,确保不抛异常
尽可能延后变量定义式的出现时间,这样可增加程序清晰度和改善程序效率
当定义一个变量,其类型带构造析构函数,那么当执行到变量定义时,便要承受构造成本;离开作用域,则要有析构成本。
尽量少做转型动作
四种转型
- const_cast
(expression)
通常用来将对象的常量性移除
const int constant = 21; const int* const_p = &constant; int* modifier = const_cast<int*>(const_p); *modifier = 7; /** constant: 21 const_p: 7 modifier: 7 **/应用场景:我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数
void Printer (int* val,string seperator = "\n") { cout << val << seperator; } int main(void) { const int consatant = 20; //Printer(consatant);//Error: invalid conversion from 'int' to 'int*' Printer(const_cast<int *>(&consatant)); return 0; }使用const_cast去除const限定的目的绝对不是为了修改它的内容,只是出于无奈。
- dynamic_cast
(expression)
需要目标类型和源对象有一定的关系:继承关系
更准确的说,dynamic_cast是用来检查两者是否有继承关系。Children * daughter_d = new Children("Daughter who pretend to be my mother"); Parents * parent_d = dynamic_cast<Parents*> (daughter_d); //right, cast with polymorphism parent_d->Speak(); Parents * father_d = new Parents("Father who pretend to be a my son"); Children * son_d = dynamic_cast<Children*> (father_d); //no error, but not safe if (son_d) { son_d->Speak(); } Stranger* stranger_d = dynamic_cast<Stranger*> (daughter_d); // stranger_d为null- reinterpret_cast
(expression)
用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。
reinterpret_cast用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。
错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。typedef int (*FunctionPointer)(int); int value = 21; FunctionPointer funcP; funcP = reinterpret_cast<FunctionPointer> (&value); funcP(value);我先用typedef定义了一个指向函数的指针类型,所指向的函数接受一个int类型作为参数。然后我用reinterpret_cast将一个整型的地址转换成该函数类型并赋值给了相应的变量。最后,我还用该整形变量作为参数交给了指向函数的指针变量。
这个过程编译器都成功的编译通过,不过一旦运行我们就会得到”EXC_BAD_ACCESS”的运行错误,因为我们通过funcP所指的地址找到的并不是函数入口。
- static_cast
(expression)
static_cast不仅可以用在指针和引用上,还可以用在基础数据和对象上
用static_cast来处理的转换就需要两者具有”一定的关系”了,static_cast支持指向基类的指针和指向子类的指针之间的互相转换在一个文件中将变量定义为static,则说明这个变量只能在本Package中使用;
在方法中定义一个static变量,该变量在程序开始存在直到程序结束;
类中定义一个static成员,该成员随类的第一个对象出现时出现,并且可以被该类的所有对象所使用。
Children * daughter = new Children(); Parents * mother = static_cast<Parents*> (daughter); //right, cast with polymorphism Parents * father = new Parents(); Children * son = static_cast<Children*> (father); //no error, but not safestatic_cast真正用处并不在指针和引用上,而在基础类型和对象的转换上;而基于基础类型和对象的转换都是其他三个转换运算符所办不到的。
float floatValue = 21.7; int intValue = 7; cout << floatValue / 7 << "\t\t" << static_cast<int> (floatValue)/7 <<endl; cout << intValue/3 << "\t\t" << static_cast<double> (intValue)/3 << endl; //Output: //3.1 3 //2 2.33333总结:
const_cast 常量
static_cast 基础类型对象;有继承或转换关系
dynamic_cast 继承类
reinterpret_cast 任意- 注重效率的代码避免dynamic_cast
- 如果必须转型,试着将转型隐藏于某个函数背后
- 宁可使用新转型,不要用旧转型
- const_cast
避免返回handles指向对象内部属性
避免返回包括指针、引用、迭代器指向对象内部,可增加封装性,帮助const成员函数行为像个const
为“异常安全”而努力是值得的
当异常抛出,有异常安全的函数会:
* 不泄露任何资源。保证资源被释放 * 不允许数据败坏。比如:count被加上了,但是实际内容没有加成功导致count值不正确 基本承诺:如果异常抛出,程序内任何事物仍然保持在有效状态下 强烈保证:如果异常抛出,程序状态不改变,如果成功就完全成功;如果失败就恢复原状 不抛保证:绝不抛出异常透彻了解inlining的里里外外
inlining化可以免除函数调用成本
背后含义是将对此函数的每一个调用都以函数本体替换之,过度热衷inlining会造成程序体积太大,降低命中率,从而影响效率
* 将大多数inlining限制在小型、频繁被调用的函数身上,可最小化潜在代码膨胀问题,使速度提升机会最大化
* 不要因为模板函数出现在头文件,就将他们声明为inline
将文件间的编译依存关系降至最低
依存关系太多,导致轻微修改,整个重新编译连接
可以分为声明式头文件和定义式头文件
程序库头文件应该是声明式头文件
避免挡住被继承来的名字
- 子类内成员的名字会遮盖父类内成员的名字
为了让父类内的名字重见天日,可使用using声明式或forwarding function
class B { public: virtual void f1(); virtual void f1(int); } class D { public: vortual void f1(); } D d; d.f1(); // 正确,调用D::f1() d.f1(1); // 错误,B::f1(int)被遮挡了 class D { public: using B::f1; // 让B内f1在D作用域内可见 virtual void f1(); } D d; d.f1(); // 正确,调用D::f1() d.f1(1); // 没问题,仍然调用B::f1(int)区分接口继承和实现继承
http://www.cnblogs.com/jerry19880126/p/3595411.html
- 纯虚函数只继承接口;
- 虚函数既继承接口,也提供了一份默认实现;
- 普通函数既继承接口,也强制继承实现。
例子
class BaseClass { public: void virtual PureVirtualFunction() = 0; // 纯虚函数 void virtual ImpureVirtualFunction(); // 虚函数 void CommonFunciton(); // 普通函数 }; void BaseClass::PureVirtualFunction() { cout << "Base PureVirtualFunction" << endl; } void BaseClass::ImpureVirtualFunction() { cout << "Base ImpureVirtualFunciton" << endl; } class DerivedClass1: public BaseClass { void PureVirtualFunction() { cout << "DerivedClass1 PureVirturalFunction Called" << endl; } }; class DerivedClass2: public BaseClass { void PureVirtualFunction() { cout << "DerivedClass2 PureVirturalFunction Called" << endl; } }; int main() { BaseClass *b1 = new DerivedClass1(); BaseClass *b2 = new DerivedClass2(); b1->PureVirtualFunction(); // 调用的是DerivedClass1版本的PureVirtualFunction b2->PureVirtualFunction(); // 调用的是DerivedClass2版本析PureVirtualFunction b1->BaseClass::PureVirtualFunction(); // 当然也可以调用BaseClass版本的PureVirtualFucntion return 0; }绝不重新定义继承而来的non-virtual函数
如果子类真的要重定义这个函数,那么说明父类的这个函数不能满足子类的要求,这就与每一个子类都是父类的原则矛盾了。
class BaseClass { public: void NonVirtualFunction() { cout << "BaseClass::NonVirtualFunction" << endl; } }; class DerivedClass: public BaseClass { public: void NonVirtualFunction() { cout << "DerivedClass::NonVirtualFunction" << endl; } }; int main() { DerivedClass d; BaseClass* bp = &d; DerivedClass* dp = &d; bp->NonVirtualFunction(); // 输出BaseClass::NonVirtualFunction dp->NonVirtualFunction(); // 输出DerivedClass::NonVirtualFunction }绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
enum MyColor { RED, GREEN, BLUE, }; class Shape { public: void virtual Draw(MyColor color = RED) const = 0; // color = RED是静态绑定 }; class Rectangle: public Shape { public: void Draw(MyColor color = GREEN) const { cout << "default color = " << color << endl; } }; class Triangle : public Shape { public: void Draw(MyColor color = BLUE) const { cout << "default color = " << color << endl; } }; int main() { Shape *sr = new Rectangle(); Shape *st = new Triangle(); sr->Draw(); // 输出为 0 RED st->Draw(); // 输出为 0 RED delete sr; delete st; }如果一定要为虚函数采用默认值,那么只要在父类中设定就可以了
class Shape { public: void DrawShape(MyColor color = RED) { Draw(color); } private: virtual void Draw(MyColor color) const = 0 { cout << "Shape::Draw" << endl; } }; class Rectangle: public Shape { private: void Draw(MyColor color) const { cout << "Rectangle::Draw" << endl; } }; class Triangle : public Shape { private: void Draw(MyColor color) const { cout << "Triangle::Draw" << endl; } }; int main() { Shape *sr = new Rectangle(); Shape *st = new Triangle(); cout << "sr->DrawRectangle() = "; // Rectangle::Draw sr->DrawShape(); cout << "st->DrawTriangle() = "; // Triangle::Draw st->DrawShape(); delete sr; delete st; }通过复合塑模出has-a或者is-implemented-in-terms-of
如果说public是一种is-a的关系的话,那么复合就是has-a的关系。直观来说,复合就是在一个类中采用其他类的对象作为自身的成员变量,可以举个例子,像下面这样:
class Person { private: string Name; // 复合string类型的变量 PhoneNumber HomeNumber; // 复合PhoneNumber对象 PhoneNumber TelephoneNumber; };明智而审慎地使用private继承
public继承在子类中保持父类的访问权限,即父类中是public的成员函数或成员变量,在子类中仍是public,对private或者protected的成员函数或成员变量亦是如此;
但private继承则不是这样了,它破坏了父类中的访问权限标定,将之都转成private,这对子类本身并无影响(照常访问),但却影响了子类的子类,子类的子类将无法访问这些声明/定义在爷爷辈类的成员变量或成员函数。
明智而审慎地使用多重继承
使用多重继承过程容易碰到的问题就是名字冲突
class Base1 { public: void fun(){} }; class Base2 { private: void fun(){} }; class Derived : public Base1, public Base2 {}; int main() { Derived d; d.fun(); // error C2385: 对“fun”的访问不明确 return 0; }能不用多重继承的时候,就尽量不用它,用复合+单继承往往能达到目的。
多重继承比单一继承更复杂。它可能导致新的歧义性,以及对virtual继承的需要。
virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
了解隐式接口和编译期多态
隐式接口是相对于函数签名所代码的显式接口而言的。
显式接口由函数签名式构成,隐式接口由有效的表达式组成。
template <class T> void TemplateFunction(T& w) { if(w.size() > 10){…} }“运行时多态”伴随着virtual关键字,本质是一个虚表和虚指针,在类对象构造时,将虚指针指向了特定的虚表,然后运行时就会根据虚表的内容进行函数调用。
“编译期多态”发生在编译阶段,实际上就是template
这个T的替换,它可以被特化为int,或者double,或者用户自定义类型,这一切在编译期就可以决定下来T到底是什么,编译器会自动生成相应的代码(把T换成具体的类型) 运行时多态是决定“哪一个virtual函数应该被绑定”,而编译期多态决定“哪一个重载函数应该被调用”。
1. class和template都支持接口与多态; 2. 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期; 3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。了解typename的双重意义
第一重含意我们既可以这样写:
template <class T>也可以这样写
template <typename T>第二重含意其实不大能遇到,因为这个依赖于编译器
class SampleClass { public: typedef int MyInt; // static const int MyInt = 3; }; int main() { SampleClass::MyInt *b = new int(3); // 有的编译器会报错 }有的编译器会报error,这是因为编译器在遇到这句代码上会产生歧义,因为它可以将MyInt作为SampleClass的一个静态对象来看(可以看程序被注释掉的代码的地方),这样就变成了一个静态对象乘以b了。为了解决这个二义性,在前面加上typename,这样就会强制让编译器将之视为一个类型,而不是静态变量了。
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或者member initialization list内使用typename
class A: public B::NestedClass{}; // 正确 class A: public typename B::NextedClass(){}; // 错误学习处理模板化基类的名称
静态多态就是模板的技术了,代码如下:
class CompanyA { public: void SendClearText(){} void SendEncryptedText(){} }; class CompanyB { public: void SendClearText(){} void SendEncrypedText(){} }; template <class T> class MsgSender { public: void SendClearText(){} }; template <class T> class MsgSenderWithLog: public MsgSender<T> { public: void SendClearTextWithLog() { // Logs SendClearText(); // 有的编译器会编不过这段代码,这是因为在模板技术中存在全特化的概念 } }; int main() { MsgSenderWithLog<CompanyA> MsgSender; MsgSender.SendClearTextWithLog(); }为了让更多的编译器放弃这种全特化的忧虑,提供了三种解决方法:
void SendClearTextWithLog() { // Logs this->SendClearText(); // 这下能编译通过了 }方法二:
在子类中声明using MsgSender<T>::SendClearText; 编译器报error本质是不进行模板父类域的查找,所以这里using了父类的一个函数名,强制编译器对之进行查找。方法三:
将SendClearText()指明为MsgSender<T>::SendClearText()。了解模板元编程
元编程本质上就是将运行期的代价转移到编译期,它利用template编译生成C++源码
template <int N> struct Factorial { enum { value = N * Factorial<N - 1>::value }; }; // 特化版本 template <> struct Factorial<0> { enum { value = 1 }; }; int main() { cout << Factorial<5>::value << endl; // 输出120 }在编译期,Factorial<5>::value就被翻译成了5 * 4 * 3 * 2 * 1,在运行期直接执行乘法即可。
元编程有何优点?
1. 以编译耗时为代价换来卓越的运行期性能,因为对于产品级的程序而言,运行的时长远大于编译时长。 2. 将原来运行期才能发现的错误提前到了编译期,要知道,错误发现的越早,代价越小。元编程有何缺点?
1. 代码可读性差,写起来要运用递归的思维,非常困难。 2. 调试困难,元程序执行于编译期,不能debug,只能观察编译器输出的error来定位错误。 3. 编译时间长,运行期的代价转嫁到编译期。 4. 可移植性较差,老的编译器几乎不支持模板或者支持极为有限。了解new_handler的行为
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
不要轻视编译器警告
争取无任何警告
warning C4068: 未知的杂注
在上面添加一行 #pragma warning(disable:4068)
warning C4018: “<”: 有符号/无符号不匹配
出错代码: for(int j=0;j<detector.size();j++)
出错原因分析: detector 是一个Vector容器 ,detecot.size() 被定义为: unsigned int 类型, 而j是int 类型
错误改正: 定义j为unsigned类型后就可以了
warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。
原因是换行符
把对应的文件另存为,会弹出另存为对话框,然后点击 保存 按钮右边的下三角,会弹出下拉菜单,有保存和编码保存两个选项,再点击编码保存。
在编码菜单中选择要保存的编码:Unicode(UTF-8 带签名)- 代码页65001,然后点击确定即可。
熟悉包括TR1在内的标准程序库
STL 标准模板库,iostreams,locales,C99
熟悉Boost
概念
vector和list的区别
vector 插入和删除操作时间复杂度为o(n);读取时间复杂度为o(1)
list 插入和删除操作时间复杂度为o(1);读取时间复杂度为o(n)
vector中去除重复元素
sort(v.begin(),v.end()); v.erase(unique(v.begin(), v.end()), v.end());map 修改value的值
map[key] = newValue;map 取值
map<int, string> ID_Name; //ID_Name中没有关键字2016,使用[]取值会导致插入 //因此,下面语句不会报错,但打印结果为空 cout<<ID_Name[2016].c_str()<<endl; //使用at会进行关键字检查,因此下面语句会报错 ID_Name.at(2016) = "Bob";判断父类对象到底是哪一个子类的对象
Class ClassApple 继承了Class ClassFruit dynamic_cast<ClassApple*>(pObj)//返回有效指针 dynamic_cast<ClassApple*>(pObj)//返回NULL把一个vector容器内容追加到另一个vector容器后面
int a[5] = {1, 2, 3, 4, 5}; std::vector<int> v1(a, a + 3), v2(a + 3, a + 5); v1.insert(v1.end(), v2.begin(), v2.end()); // 把v2加到v1末尾vector如何根据索引删除一个元素
if(idx <= vec.size()) { vec.erase(vec.begin() + idx); }创建对象数组
//1、 Student* stu[3]; stu[0]=new Student(); stu[1]=new Student(); stu[2]=new Student(); //2、 Student* stu=new Student[3];vector做形参传递问题
void useVec(vector<int> &vect){ vect.push_back(1); }std:map<string, A>
stl是值复制,可以把A变成指针A*,避免构造临时A对象
map的[]操作符会利用默认构造函数产生一个pair的第二个元素A
在元素的转移过程中会出现构造和析构,应该写出默认复制构造函数和默认=操作符,这样构造和析构才是对称的
class ST { int a; int b; ST() //默认构造函数 { a = 0; b = 0; } ST& operator=(const ST& s)//重载运算符 { set(this,(ST*)&s) } ST(const ST& s)//复制构造函数 { *this = s; } void set(ST* s1,ST* s2)//赋值函数 { s1->a = s2->a; s1->b = s2->b; } }C++中有三种创建对象的方法
A a(1); //栈中分配
A b = A(1); //栈中分配
A* c = new A(1); //堆中分配
第一种和第二种没什么区别,一个隐式调用,一个显式调用,两者都是在进程虚拟地址空间中的栈中分配内存,而第三种使用了new,在堆中分配了内存,而栈中内存的分配和释放是由系统管理,而堆中内存的分配和释放必须由程序员手动释放。采用第三种方式时,必须注意一下几点问题:a.new创建类对象需要指针接收,一处初始化,多处使用 b.new创建类对象使用完需delete销毁 c.new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间 d.new对象指针用途广泛,比如作为函数返回值、函数参数等 e.频繁调用场合并不适合new,就像new申请和释放内存一样 f.栈的大小远小于堆的大 g.栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率 比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在 堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会 分 到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
sqlite3
CPP封装的Sqlite3
https://www.codeproject.com/Articles/6343/CppSQLite-C-Wrapper-for-SQLite
SQLite的两个加密函数使用起来非常的简单,下面分情况说明:
1、 给一个未加密的数据库添加密码:如果想要添加密码,则可以在打开数据库文件之后,关闭数据库文件之前的任何时刻调用sqlite3_key函数即可,该函数有三个参数,其中第一个参数为数据库对象,第二个参数是要设定的密码,第三个是密码的长度。例如:sqlite3_key(db,"1q2w3e4r",8); //给数据库设定密码1q2w3e4r 注:如果数据库没有加密,执行此函数后进行数据库操作反而会出现“此数据库已加密或不是一个数据库文件”的错误?经测试,只能在新建数据库时设置密码! 2、 读取一个加密数据库中的数据:完成这个任务依然十分简单,你只需要在打开数据库之后,再次调用一下sqlite3_key函数即可,例如,但数据库密码是123456时,你只需要在代码中加入sqlite3_key(db,"123456",6); 3、 更改数据库密码:首先你需要使用当前的密码正确的打开数据库,之后你可以调用sqlite3_rekey(db,"112233",6) 来更改数据库密码。 4、删除密码:也就是把数据库恢复到明文状态。这时你仍然只需要调用sqlite3_rekey函数,并且把该函数的第二个参数置为NULL或者"",或者把第三个参数设为0。 bool CppSQLite3DB::open(const char* szFile) { int nRet = sqlite3_open(szFile, &mpDB); if (nRet != SQLITE_OK) { const char* szError = sqlite3_errmsg(mpDB); throw CppSQLite3Exception(nRet, (char*)szError, DONT_DELETE_MSG); } else { if (ENCRYPTED) { nRet = sqlite3_rekey(mpDB, DATABASEPWD, strlen(DATABASEPWD)); if (nRet != SQLITE_OK) { sqlite3_close(mpDB); if (SQLITE_OK != sqlite3_open(szFile, &mpDB)) { return false; } } sqlite3_key(mpDB, DATABASEPWD, strlen(DATABASEPWD)); } } setBusyTimeout(mnBusyTimeoutMs); return true; }
工具
开源
OpenFrameworks:一个用C++编码的跨平台开源工具包
很强!!!!有各个平台,包括android的版本
OF论坛网址:http://forum.openframeworks.cc/
GitHub上的网址:https://github.com/openframeworks/openFrameworks
指导手册:https://github.com/openframeworks/openFrameworks/blob/master/CONTRIBUTING.md
hiberlite:用于Sqlite3的C++对象关系映射
最全的C++资源大全
实战
经验总结
测试消耗时间,用于测试性能
static float calculateDeltaTime( struct timeval *lastUpdate ) { struct timeval now; gettimeofday( &now, nullptr); float dt = (now.tv_sec - lastUpdate->tv_sec) + (now.tv_usec - lastUpdate->tv_usec) / 1000000.0f; return dt; } struct timeval now; gettimeofday(&now, nullptr); log("1 ms:%f", calculateDeltaTime(&now));float型变量和“零值”比较的方法:
const float EPSINON = 0.00001; if ((x >= - EPSINON) && (x <=EPSINON))浮点型变量并不精确,其中EPSINON是允许的误差(即精度),所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则是错误的。
因为1.0在计算机中可能存为0.999999或1.00001等,很难恰好是1.0Vs2013增加静态库工程
http://www.cppblog.com/zdhsoft/archive/2014/05/11/206890.html
建立lib项目
a)解决方案中新建项目-选择“静态库(通用windows)” b)项目建立在cocos2dx的根目录中 c)添加cocos2dx宏选择视图-其他窗口-属性管理器
选择项目里的-Debug|win32和Debug|x64-右键选择-添加现有属性表-到目录选择属性文件cocos\2d\cocos2dx.props
d)其他项目引用此lib项目选择项目里的-Debug|win32和Debug|x64-右键选择-添加新项目属性表-选择“用户宏”-添加宏-比如名称“GamedoLib”-值“$(MSBuildThisFileDirectory)......\gamedolib\”-勾选“将此宏设置为。。。。。”
C/C++-常规-附加包含目录-添加“$(GamedoLib)gamedocommon\Classes\Util”
切换到“解决方案资源管理器”-选择项目的“引用”-右键“添加引用”-选择“lib项目名称”
vs2015编译win10时注意:
a)使用x86编译
b)配置属性-C/C++-常规-附加包含目录
平台(P)要选择“所有平台”error C2338: /RTCc rejects conformant code错误解决
双击之后,会进入这里:
#ifdef _RTC_CONVERSION_CHECKS_ENABLED #ifndef _ALLOW_RTCc_IN_STL static_assert(false, "/RTCc rejects conformant code, " "so it isn't supported by the C++ Standard Library. " "Either remove this compiler option, or define _ALLOW_RTCc_IN_STL " "to acknowledge that you have received this warning."); #endif /* _ALLOW_RTCc_IN_STL */ #endif /* _RTC_CONVERSION_CHECKS_ENABLED */Smaller Type check (/RTCc):数据截断的检测
但是STL不允许这个检查,按上面解释来说,要么关闭这个选项,要么定义
_ALLOW_RTCc_IN_STL宏配置属性-C/C++-代码生成-较小类型检查-否
无法解析的外部符号 public: static
C++ static变量在头文件中定义后,应在类的声明之外,cpp中重新定义一次
// h class CGlobalVa { public: static double m_length; }; // cpp double CGlobalVa::m_length=0;