C++对C的改进
- 用引用来解决指针的问题
- 用 namespace 来解决名字空间冲突的问题
- 通过 try-catch 来解决检查返回值编程的问题
- 用 class 来解决对象的创建、复制、销毁的问题,从而可以达到在结构体嵌套时可以深度复制的内存安全问题
- 通过重载操作符来达到操作上的泛型
- 通过模板 template 和虚函数的多态以及运行时识别来达到更高层次的泛型和多态
- 用 RAII、智能指针的方式,解决了 C 语言中因为需要释放资源而出现的那些非常 ugly 也很容易出错的代码的问题
- 用 STL 解决了 C 语言中算法和数据结构的 N 多种坑
泛型
- 类型的本质
- 类型是对内存的一种抽象。不同的类型,会有不同的内存布局和内存分配的策略
- 类型系统的出现主要是对容许混乱的操作加上了严格的限制,以避免代码以无效的数据使用方式编译或运行
- 例如,整数运算不可用于字符串;指针的操作不可用于整数上
- 类型的产生和限制,虽然对底层代码来说是安全的,但是对于更高层次的抽象产生了些负面因素
- 比如在 C++ 语言里,为了同时满足静态类型和抽象,就导致了模板技术的出现,带来了语言的复杂性
- 泛型的本质
- 屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型
- 要做到泛型,我们需要做下面的事情
- 标准化掉类型的内存分配、释放和访问
- 标准化掉类型的操作
- 比如:比较操作,I/O 操作,复制操作
- 标准化掉数据容器的操作
- 比如:查找算法、过滤算法、聚合算法
- 标准化掉类型上特有的操作
- 需要有标准化的接口来回调不同类型的具体操作
- C++对泛型的实现
- 通过类中的构造、析构、拷贝构造,重载赋值操作符,标准化(隐藏)了类型的内存分配、释放和复制的操作
- 通过重载操作符,可以标准化类型的比较等操作
- 通过 iostream,标准化了类型的输入、输出控制
- 通过模板技术(包括模板的特化),来为不同的类型生成类型专属的代码
- 通过迭代器来标准化数据容器的遍历操作
- 通过面向对象的接口依赖(虚函数技术),来标准化了特定类型在特定算法上的操作
- 通过函数式(函数对象),来标准化对于不同类型的特定操作
容器
- 所有容器的公共操作
- == 和 !=
- empty()
- size()
- clear()
- begin() 和 end()
- insert()
- erase()
- 因为 delete 为关键字不能用,所以改用 erase
- 容器中不能存引用类型的数据
顺序容器
- 一般指 vector,list或deque
- 随机访问,尾端插入,不删除选 vector
- 任意位置插入删除,顺序访问选 list
- 前端插入或删除,随机访问选 deque
- deque的底层实现:https://blog.csdn.net/baidu_28312631/article/details/48000123
- STL中的 优先队列底部容器一般用deque或者 vector
- vector, string等顺序容器也提供 erase函数以实现随机删除,但是这一操作会复制被删除元素后面所有的元素,所以效率很低
- 五种初始化方式
- 直接产生空容器
list<string>;
- 产生特定大小的容器
vector<string> strVec(10);
- 产生特定大小的容器并指定统一初值
vector<int> intVec(5,0);
- 复制已经存在的容器
list<string> newList = oldList;
- vector,list, string现在都已支持列表初始化
- 直接产生空容器
list<int> v{1, 2, 3, 4, 5};
vector<int> v{1, 2, 3, 4, 5};
string v{'a', 'b', 'c'};
- 基本操作
- push_back()
- pop_back()
- 注意这个方法没有返回值
- push_front()
- vector没有这个方法
- pop_front()
- vector没有这个方法
- 没有返回值
- back()
- front()
- stack 和 queue 不支持遍历操作
- 也就是说不支持范围for循环,也不支持find操作
关联容器
- map
- 有两个成员 first和second
- first 是key, second 是value
- 使用下标访问map容器时,如果key不在map中,会被自动加入map
- 因此查找一个key是否在map中应该调用 find方法
- 也可以用 count方法代替,因为map中所有key都最多有一个
字符串(string)
- 加法操作两边允许的类型有string, char* 和char
- string不可直接与int类型相加,而要转换为一个char类型(+‘0’)
- 字符串拼接时(即+=操作),复杂度与新加入的字符串长度成线性正比
- swap交换string的复杂度为O(1),因为只需要交换内部的char*指针即可
指针
- 在不确定指针非空前,if语句使用指针时首先应当判断指针存在性,否则会会出现运行错误
if (pt && ...)
- 指针和引用的差异就在于指针可能不指向某个对象(nullptr),而引用一定要显式绑定当一个对象
- 另一个差异是引用一旦绑定就不可更改对象
- ptr+ 1前进的地址与 ptr 是什么类型的指针有关
- 如果 ptr 是int(4byte长度)指针,则地址事实上+4
函数
- 函数指针数组的写法
int (*pt[])(double, string)
- 内联函数的实现必须放在头文件中
- 因为内联是之间展开代码,无法实现跨编译单元编译
- 内联符号应该与函数的定义放在一起,而不是声明
- 将函数返回值赋值的过程
- 生成一个临时对象,调用拷贝构造函数将该对象初始化
- 将该临时对象通过赋值构造函数赋给左操作符
- 如果不是移动对象的话,还要析构临时对象
- 函数内部的静态变量
- 函数中每次调用都要使用的且值是固定的变量
- 用来代替全局变量,减少变量被误用或者修改的可能
- 一般用来表示字符串
迭代器
-
为了实现容器独立,不要直接在容器上操作,而是借助指针标识要迭代的范围
- 比如用first和last指针来代替数组(容器)名和大小
- 其中last指向最后一个元素的后一个地址
-
为了防止容器为空时指针的操作导致运行时错误,应该将取第一个/最后一个元素的操作封装为一个函数
Val* begin(const vector<Val> &vec) { return vec.empty() ? 0 : &vec[0]; }
-
然而不同类型的容器指向下一个元素的操作是不同的(比如vector和list)
- 为了统一语法,我们在指针行为之上多提供一层抽象,于是产生了iterator类
- 迭代器还能省略判断是否为空容器的步骤
- 为了统一语法,我们在指针行为之上多提供一层抽象,于是产生了iterator类
-
iterator的定义应该提供容器类型和 iterator所指向元素类型两个信息
vector<string>::iterator iter;
list<int>::const_iterator constIter;
-
迭代器的失效
- 插入和删除操作都会使得容器内部结构改变而导致迭代器失效
- vector的push_back()方法照理说不会使得begin()方法失效,然而当该操作使得vector的capacity大小改变的时候,整个vector会被拷贝到另一块地方,导致迭代器失效
- 关联容器在删除时只会使得当前的迭代器失效,而vector和deque会使得后面的迭代器都是失效
-
迭代器适配器
- 包含在头文件 iterator 中
- 是产生迭代器的函数
- 插入适配器让关联的容器动态的增长,从而使得迭代器能够被安全地用做复制算法的目的地
- 参数类型是容器而不是迭代器
// 错误:vec.end()中没有元素
copy(bottom.begin(), bottom.end(), vec.end());
// 正确
copy(bottom.begin(), bottom.end(), back_inserter(vec));
- 迭代器类型
- 流迭代器
- 输入迭代器
- 只能输入,单向顺序访问
- 输出迭代器
- 只能输出,单向顺序访问
- 输入迭代器
- 正向迭代器
- 输入输出都可,单向顺序访问
- 双向迭代器
- 输入输出都可,双向顺序访问
- 随机访问迭代器
- 输入输出都可,随机访问
- 流迭代器
随机访问迭代器
- 所谓随机访问,是按照数组的方式在内存中顺序存放,只需要根据首地址和相应下标就能寻址到相应的元素
- 所有常用容器中,只有 vector, deque, string, array等少数几个支持随机访问
- 在不是随机访问的容器中,不支持下列操作:
//v为容器实例名,it为相应迭代器名
v[n]; //下标访问
it += n;//移动迭代器
auto newIt = it - n; //前面第n个元素的迭代器
auto n = newIt - it; //返回迭代器之间的元素数量
it < newIt; //迭代器比较
- 要注意的是,所有迭代器都支持下面的操作
it1 == it2; //判断指向相同的元素
it1 != it2; //判断指向不同的元素
it++; //自增
-
然而自减运算符只有随机迭代器和双向迭代器支持
-
所有迭代器都不支持两个迭代器相加的操作,所以想要得到两个迭代器的中值,不可直接相加除2
auto begin = vec.begin(); auto end = vec.end(); // 错误 auto mid = (begin + end) / 2; // 正确 auto mid = begin + (end - begin) / 2;
流迭代器
- 定义在 iterator 头文件中
- 需要指定类型参数,也就是说输入或者输出数据需要是相同类型
- 输入迭代器
istream_iterator<int>(cin) //相当于begin
istream_iterator<int() //相当于end,迭代器到达文件末尾或者出错则和这个值相等
//举例
vector<int> v;
copy(istream_iterator<int>(cin), istream_iterator<int>(), back_inserter(v));
- 输出迭代器
ostream_iterator<int>(cout, "\n") //第二个参数为分隔符,如果省略则没有分隔符
//举例
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
类
-
在类的声明中定义的成员函数自动被视为内联函数
-
当用一个类对象初始化另一个类对象时会自动调用拷贝构造函数
-
成员函数会隐式包含this指针的参数(如果是const函数则是const指针)
- 由于这个隐含参数的存在,我们可以对const和非const函数进行重载,即使他们的参数列表看起来一样
-
静态类
-
静态成员对象
- 只被创造一次,是唯一的
- 很像全局对象
- 该类的每一个实例都可以访问该成员对象
- 只被创造一次,是唯一的
-
静态成员函数
-
只能访问静态类成员对象
-
由于不存在与任意对象实例中,因此不能使用this关键词
-
可以不依赖特定实例调用,直接在该函数前加上类作用域即可
bool isElement = Object::isElement(val);
-
无法被声明为虚函数
-
-
静态成员只能在内部声明,必须在外部单独定义
- 定义方法类似成员函数的定义,需要写出类型和类作用符
const int Object::num; vector<int> Object::vec(num);
-
如果为const类型静态变量则可以在声明类时同时定义,但也只能用常量定义
- 但即使这样最好也要显示在外部声明下,例如上面的num
-
-
友元
- 如果要声明一个类的成员函数为另一个类的友元,要在之前在另一个类声明该类
- 如果直接声明友元类则无需预先声明该类
- 友元关系不能被继承
- 如果要声明一个类的成员函数为另一个类的友元,要在之前在另一个类声明该类
构造函数
-
explicit构造函数
- =号在C++中不仅能用来赋值,还可以用来初始化,这被称为隐式初始化
- 隐式初始化存在的问题
- 没有预料的隐式类型转化
- 当其余的参数有默认值时,会默认构造
- explicit前缀可以禁止隐式初始化从而避免这一问题
- 一般当参数全部成为类中成员的时候,不必声明explicit;而有一些参数决定了对象的结构的时候,就需要声明explicit
class Test { double A; int B; public: Test(double a, int b = 1): A(a), B(b) {} } //隐式构造 Test t = 1;
-
拷贝构造函数
- 参数是与类同一类型的常量引用,其余与构造函数一致
- 当类中含有指针成员时,我们必须把指针指向的数据一并复制,而不能仅仅复制指针
- 否则就有两个变量指向同一块内存地址
- 与此同时一定要同时重载赋值运算符
-
赋值构造函数和析构函数都需要删除原来对象中的元素并释放占用的空间,这部分代码可以封装成一个私有函数并被两个函数调用
-
三位一体规则
- 一般来说,析构函数,拷贝构造函数以及赋值运算符是同时存在或者不存在的
- 如果其中一个函数不需要,一般另外两个也不需要。同理,一个需要则另外两个都需要显式构造
- 比如string类不需要析构函数,因为其本身没有分配内存能力,因此也不需要定义拷贝构造函数,直接使用默认定义即可
运算符操作
-
重载运算符
- 如果把运算符用外部函数实现,需要的参数和运算符的操作数一样多
- 第一个参数一定是左操作符,第二个参数一定是右操作符
- 如果用类的成员函数实现,需要的参数少一个
- 左操作符一定是该类对象
- 索引运算符,赋值运算符等一般是用成员函数实现的
- 因为这些运算符会改变对象
- 重载的运算符一般来说返回一个引用
- 一时避免拷贝很大的对象
- 二是可以实现连级运算
- 比如
cin >>a>>b>>c;
- 比如
- 注意加减乘除不要返回引用
//相等运算 bool operator==(const Object&) const; // ++i objectIterator& operator++(); //i++ //int参数无意义,只是为了区分前置和后置 //由于很多编译器在参数没有被用到时会有警告,所以参数未命名 objectIterator operator++(int); //输出运算 //不设计为成员函数,否则在写输出时必须为 obj << cout 的形式 //return对象为参数os本身 //由于该函数为非成员函数,所以要访问成员变量需要成为友元,避免这种写法的办法就是让该函数调用成员函数 //当然,这个成员函数必须是public的,如果是private的还是要声明为友元函数 ostream& operator<<( ostream & os, const Object &rhs) { return rhs.print(os); }
- 如果把运算符用外部函数实现,需要的参数和运算符的操作数一样多
-
二元运算符
- 使用成员函数构造
- 一般用于修改对象的内部状态
- 操作符左边的对象不能进行类型转换
- 一般用于非对称操作符,比如 +=
- 使用非成员函数构造
- 可能需要声明友元
- 操作符左右对象都能自动类型转换
- 一般用于对称操作符,比如 **+**和 >>, <<
- 由于 *char **对象不支持加法操作,只有string类支持,因此链接多个字符串的时候,每个加号左右至少有一个string对象
- 使用成员函数构造
-
类型转换操作符
-
用来将调用该运算符的类型转换为指定的类型
-
语法一般为 operator + 类型, 比如 operator double() const;
-
缺陷
- 比如一个string类型想要通过cout输出,则需要转换成char*类型,但是这样会暴露内部数据
- 改用**const char* **可以解决上述问题,但是如果string类突然被删除了,该指针就会指向无效地址
- 重新开辟一块新内存给指针可以解决上述问题,但是有时转换调用的发生时隐式的,此时没有指针给我们释放这块内存
//假设string采取我们的方法实现类型转换操作符 //此时我们无法释放内存 string str; ifstream is(str);
- 因此,真正的string**类只允许我们显示地调用c_str(), data(), copy()**等方法进行类型转换
-
继承和多态
-
继承让相关联的类共享共通的接口
-
当子类和父类拥有同名成员时,子类成员会掩盖父类成员
- 如果必须要调用父类版本成员,可以使用父类的类作用域前缀显式调用
-
多态使得我们得以用一种与类型无关的方式来操作类对象
-
派生对象创建时基类和派生类的构造器都会被执行(销毁时析构器也都会执行)
-
动态绑定
- 对于一个类方法的调用,到底调用哪个派生类的方法要到运行时才被解析
- 默认成员函数在编译时静态解析,通过在声明前加上 virtual关键字使其子运行时动态解析
- 析构器应该声明前加上 virtual以实现运行时解析(当然基类的析构函数也会被执行)
- 有时候我们不想通过动态绑定来调用某一方法,这是就要在方法前加上我们想调用类的类作用域符,此时显式调用该类方法的版本
-
构造函数不支持 virtual
- 虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数
- 虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用
-
虚函数
- 对于虚函数,子类可以不实现,直接用基类的版本就可,但是对于纯虚函数,由于基类并没有实现,所以每个子类都必须实现
- 虚函数的属性会被继承,因此子类无需再显式声明虚函数
- 虚函数机制的实现只能通过指针和引用
- 例如参数是一个基类实例而非引用或者指针,那么该参数调用的方法一定为基类方法,即使传入的实参是一个子类对象
- 而子类对象自己多加的成员变量和函数都会被切割掉
- 原因是该参数在函数调用时就分配了容纳实际对象的空间,而引用或指针只是分配了地址
-
前面我们知道通过虚函数和指针可以实现多态,然而有一个问题是当我们使用动态内存存储对象,在用完对象调用delete释放动态分配的内存时,会首先调用对象的析构函数,但是我们应该调用哪个类型的析构函数呢?
- 这一问题可以通过虚拟析构函数解决,虚拟析构函数可以在运行时决定析构哪个类型的对象并且释放相应大小的内存
- 只要基函数声明虚拟析构函数即可,子函数自动继承,无需声明
- 因此,如果类中定义了虚函数,那么必须显式声明虚拟析构函数
- 否则delete释放的内存会变小(子类比基类大),造成内存泄漏
-
上面如此种种都要用户去管理,很麻烦。于是引入了句柄类的概念
- 句柄类实质就是在普通的指针外面加一个类的封装
- 成员变量仅有类指针和引用计数指针两个
- 是智能指针的雏形
-
如果已经可以提前知道将要调用哪个版本的虚函数,可以通过类作用域前缀显式声明
- 这样可以使得函数在编译时而不是运行时就被解析,减少运行时成本
// Fibonacci类是一个子类,当我们调用该类的函数时,该函数内部使用的虚拟函数(gen_elements)必定是对应Fibonacci版本的,无需使用虚函数机制等到运行时才解析,因此我们用过前缀跳过虚函数机制 int Fibonacci::element(int pos) { Fibonacci::gen_elements(pos); return elems[pos-1]; }
模板
- 模板函数
- 函数定义存在形参的时候,在调用模板函数时无需显式声明模板参数,编译器会根据传入的实参自动推断
- 但是当函数定义不存在形参的时候,调用时就必须显式声明模板参数
//定义
template <int len, int pos>
class Object{...};
//使用
Object<10, 0> obj;
-
非类型参数
- 模板的参数不一定是某种类型,也可以是常量表达式
- 一般是整数(或者枚举),也可以是全局变量或函数的地址
- 所以想传入浮点数或者类对象时,可以先定义一个全局变量,然后传入地址
- 这样做是为了将一些运行时的计算转移到了编译时,节省了运行时开销和内存占用
- 一般这样的编程风格叫做模板元编程
- 比如对于向量的计算可以避免for循环,缺点是导致代码膨胀
//模板 template <int length> Vector<length>& Vector<length>::operator+=(const Vector<length>& rhs) { for (int i = 0; i < length; ++i) value[i] += rhs.value[i]; return *this; } //当length实际传入为2时,编译器将上面的模板转换如下 template <> Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) { value[0] += rhs.value[0]; value[1] += rhs.value[1]; return *this; }
- 模板的参数不一定是某种类型,也可以是常量表达式
-
成员模板函数
- 如果我们想要对类中的某个成员函数使用模板,但是又不想让整个类都模板化,此时可以使用成员模板函数
- 好处是无需在调用时显示声明传入参数的类型,编译器自动匹配
#include<iostream> #include<string> using namespace std; class PrintIt { public: PrintIt(ostream &os) :_os(os) { } template <typename elemType> void print(const elemType& elem, char delimiter = '\n') { _os << elem << delimiter; } private: ostream & _os; }; int main() { PrintIt to_standard_out(cout); to_standard_out.print("hello"); to_standard_out.print(1024); string my_string("this is a string!"); to_standard_out.print(my_string); getchar(); return 0; } //输出 hello 1024 this is a string!
-
模板特化
- 在完成模板的常规定义后,我们可以通过模板特化来实现对于某些类型进行特殊的处理
- **template<>**语法表明这是一个模板特化
//常规定义 template<typename T> T *clone()(const T *tp) { return tp->clone();} //对Vec<char>类型的模板特化 template<> Vec<char> *clone(const Vec<char> *vp) { return new Vec<char>(*vp);}
-
function object(函数对象)
- 函数对象,或者叫仿函数,就是把类对象当做函数一样来用
- 一般形式就是一个类中含有一个()操作符的重载函数
- 一般用于模板参数,用途类似函数指针
- 内置的函数对象有
greater<T>, plus<T>, negate<T>
等,位于头文件 functional中
template <typename T, typename Comp = less<T>> class User { ... }; // 函数对象 class stringLen { public: bool operator()(const string& a, const string& b) { return a.size() < b.size(); } }; // 调用方式 User<string, stringLen> object; // 内置函数对象运用举例 sort(vec.begin(), vec.end(), greater<int>()); transform(vec.begin(), vec.end(), list.begin(), list.end(), multiplies<int>());
异常
- 这里的异常和segmentation fault等硬件异常不一样
- 异常是某种对象,继承与特定的异常类
class overflow
{
int index;
string name;
public:
overflow(int Index, string Name):
index(Index), name(Name);
void print(ostream &os = cerr)
{
os << "error:" << name << " at current index" << index << endl;
}
}
- 两种抛出异常的方式
throw overflow(index, name);
overflow ex(index, name);
throw ex;
-
捕获异常
- try和catch一定要一起出现,可能引发异常的代码放在try中
- throw本身可以单独出现,但是当一个函数throw异常后,函数剩余的部分不会被执行,而是直接退出函数,查看调用端是否存在异常捕获(同样调用端在该函数后的语句也不会被执行)
- 也就是说如果不处理异常,可能会造成内存泄漏
- 当抛出的异常对象和catch中的对象类型一致时异常被捕获
- 如果该异常不能现在被完整处理(或许要在上级函数继续处理)那么可以再次抛出异常
- 如果异常一直到main函数还是没有合适的catch,则会被默认处理,即中断程序
- 如果想要捕捉任何类型的异常,catch的括号中用省略号即可
try { //code } catch(overflow& ex) { ex.print(); throw; } catch(...) { exit(1); }
标准异常
- try和catch一定要一起出现,可能引发异常的代码放在try中
-
C++为很多异常情况定义了标准异常体系(比如new失败抛出bad_alloc异常)
-
这些标准异常都继承自没exception的抽象基类,且都有 **what()**方法,用于输出相关文字表述
-
我们可以将自己定义的异常类继承于exception基类
- 好处是可以被任何打算捕获标准异常的catch捕获,不用再用**catch(…)**捕获所有未考虑的异常
class overflow : public exception { int index; string name; public: overflow(int Index, string Name): index(Index), name(Name); const char* what() const { ostringstream ex_msg; static string msg; //ostringstream 可以将不同类型的数据格式化为字符串 //需要 #include <sstream> ex_msg << "error:" << name << " at current index" << index << endl; msg = ex_msg.str(); return msg.c_str(); } } //捕获 catch( const exception& ex) { cerr << ex.what() << endl; }
拾遗
- using … 也是有作用域的,如果using语句出现在大括号中,那么括号外的语句并不受此影响
- using namespace std; 最好只在.cpp文件中使用,.h文件中最好还是直接使用作用域运算符
- 应该放在引用头文件下面,否则和放在头文件中性质一样
- 如果要使用标准库中的某些函数却不想直接使用namespace,则可以用下面的语句
using std::cout;
using std::vector;
using std::copy;
-
cctype头文件为处理字符提供了很多有用的函数
isspace(c);//字符c是否是空白字符 isalpha(c);//字符c是否是字母 isdigit(c);//字符c是否是数字 ispunct(c);//字符c是否是标点 isupper(c);//字符c是否是大写字母 tolower(c);//大写变小写
-
标准错误流
- cerr流,即时输出错误信息
- clog流,增加了一个缓冲区,需要显式调用输出,多用于生成异常信息日志
-
string类型可以直接通过cout输出,但是必须包含string头文件,因为<<运算符的重载只出现在该类中
-
new函数需要做哪些工作
-
分配新的内存空间
-
用传入的T类型的默认构造函数初始化每个元素
- 因此如果我们在初始化T类型对象的元素的时候实际上进行了两次初始化
-
string类的c_str()函数返回的字符串指针实际上是string对象的内部指针
- 也就是说如果string对象被析构了,那么返回这个指针就指向一个垃圾值
- 所以最好用 strcpy函数处理这个指针
-
默认参数在声明或定义中仅能出现一次
-
如果在模板类中定义模板函数,则在外部实现的时候要单独写两遍template<>
template<typename T>
template<typename In>
typename Vec<T>::iterator Vec<T>::insert(iterator pos, In first, In last)
{...}
- 头文件依赖
- 如果cpp文件中包含了某个头文件,而该头文件被修改了,那么此cpp文件也要被再次编译
- 因此避免出现不必要的包含,以减少编译时间
- 为什么for循环默认循环变量为size_t?
- 因为size_t在不同的平台上会尽可能的保持一致的长度。一个int在各种CPU的32位系统和64位系统上可能有的是16位,32位或者64位。
- 举个例子,如果用int,你的循环要跑70000次,在32位cpu上没有问题,如果跑在16位的单片机上估计就要悲剧。
- size_t基本上可以避免这个问题,如果目标平台没有对应长度的类型,你的代码在编译的预处理阶段就会报错。
- new int[10] 和 new int[10]()的区别
- 前者未初始化,值为任意值。后者初始化了,值为0
- 但是对于有构造函数的自定义类,加不加()都会执行默认构造函数
- 括号中不可填数字,也就是说初始化数组时只能全部默认初始化为0,而不是指定值
- 宏替换可能导致重复执行的问题
# define min(x, y) ((x) > (y) ? (y) : (x))
min(i++, j++) // i,j 都被累加了两次而不是一次
min(foo(), bar()) // 两个函数都被调用了两次