for 循环

C++11这次的更新带来了令很多C++程序员期待已久的for range循环,每次看到javascript, lua里的for range,心想要是C++能有多好,心里别提多酸了。这次C++11不负众望,再也不用羡慕别家人的for range了。

C++98/03 中普通的 for 循环,语法格式:

for(表达式 1; 表达式 2; 表达式 3)
{
// 循环体
}

实例代码

#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> t{ 1,2,3,4,5,6 };
for (auto it = t.begin(); it != t.end(); ++it)
{
cout << *it << " ";
}
cout << endl;

return 0;
}

C++11 基于范围的 for 循环,语法格式:

for (declaration : expression)
{
// 循环体
}

使用基于范围的 for 循环遍历容器,示例代码如下:

#include <iostream>
#include <vector>
using namespace std;

int main(void)
{
vector<int> t{ 1,2,3,4,5,6 };
for (auto value : t)
{
cout << value << " ";
}
cout << endl;

return 0;
}

关系型容器实例

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(void)
{
map<int, string> m{
{1, "lucy"},{2, "lily"},{3, "tom"}
};

// 基于范围的for循环方式
for (auto& it : m)
{
cout << "id: " << it.first << ", name: " << it.second << endl;
}

// 普通的for循环方式
for (auto it = m.begin(); it != m.end(); ++it)
{
cout << "id: " << it->first << ", name: " << it->second << endl;
}

return 0;
}

元素只读:
通过对基于范围的 for 循环语法的介绍可以得知,在 for 循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值,但是这并不适用于所有的情况,对应 set 容器来说,内部元素都是只读的,这是由容器的特性决定的,因此在 for 循环中 auto & 会被视为 const auto & 。

#include <iostream>
#include <set>
using namespace std;

int main(void)
{
set<int> st{ 1,2,3,4,5,6 };
for (auto &item : st)
{
cout << item++ << endl; // error, 不能给常量赋值
}
return 0;
}

指针空值类型 - nullptr

C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

char *ptr = 0;
char *ptr = NULL;

C++ 中将 NULL 定义为字面常量 0,并不能保证在所有场景下都能很好的工作,比如,函数重载时,NULL 和 0 无法区分:

#include <iostream>
using namespace std;

void func(char *p)
{
cout << "void func(char *p)" << endl;
}

void func(int p)
{
cout << "void func(int p)" << endl;
}

int main()
{
func(NULL); // 想要调用重载函数 void func(char *p)
func(250); // 想要调用重载函数 void func(int p)

return 0;
}

出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改,而是另其炉灶,引入了一个新的关键字 nullptr。nullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:

int*    ptr1 = nullptr;
char* ptr2 = nullptr;
double* ptr3 = nullptr;

使用 nullptr 可以很完美的解决上边提到的函数重载问题:

#include <iostream>
using namespace std;

void func(char *p)
{
cout << "void func(char *p)" << endl;
}

void func(int p)
{
cout << "void func(int p)" << endl;
}

int main()
{
func(nullptr);
func(250);
return 0;
}

Lambda 表达式

lambda 表达式是 C++11 最重要也是最常用的特性之一,这是现代编程语言的一个特点,lambda 表达式有如下的一些优点:

  • 声明式的编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象。
  • 简洁:避免了代码膨胀和功能分散,让开发更加高效。
  • 在需要的时间和地点实现功能闭包,使程序更加灵活。
    lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};

其中 capture 是捕获列表,params 是参数列表,opt 是函数选项,ret 是返回值类型,body 是函数体。

  • 捕获列表 []: 捕获一定范围内的变量。
  • 参数列表 (): 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。
auto f = [](){return 1;}  // 没有参数, 参数列表为空
auto f = []{return 1;} // 没有参数, 参数列表省略不写

opt 选项, 不需要可以省略

  • mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
  • exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw ();

返回值类型:在 C++11 中,lambda 表达式的返回值是通过返回值后置语法来定义的。
函数体:函数的实现,这部分不能省略,但函数体可以为空。
捕获列表
lambda 表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:

  • [] - 不捕捉任何变量
  • [&] - 捕获外部作用域中所有变量,并作为引用在函数体内使用 (按引用捕获)
  • 捕获外部作用域中所有变量,并作为副本在函数体内使用 (按值捕获)
  • [=, &foo] - 按值捕获外部作用域中所有变量,并按照引用捕获外部变量 foo
  • [bar] - 按值捕获 bar 变量,同时不捕获其他变量
  • [&bar] - 按引用捕获 bar 变量,同时不捕获其他变量
  • [this] - 捕获当前类中的 this 指针
    让 lambda 表达式拥有和当前类成员函数同样的访问权限
    如果已经使用了 & 或者 =, 默认添加此选项
#include <iostream>
#include <functional>
using namespace std;

class Test
{
public:
void output(int x, int y)
{
auto x1 = [] {return m_number; }; // error
auto x2 = [=] {return m_number + x + y; }; // ok
auto x3 = [&] {return m_number + x + y; }; // ok
auto x4 = [this] {return m_number; }; // ok
auto x5 = [this] {return m_number + x + y; }; // error
auto x6 = [this, x, y] {return m_number + x + y; }; // ok
auto x7 = [this] {return m_number++; }; // ok
}
int m_number = 100;
};

x1:错误,没有捕获外部变量,不能使用类成员 m_number
x2:正确,以值拷贝的方式捕获所有外部变量
x3:正确,以引用的方式捕获所有外部变量
x4:正确,捕获 this 指针,可访问对象内部成员
x5:错误,捕获 this 指针,可访问类内部成员,没有捕获到变量 x,y,因此不能访问。
x6:正确,捕获 this 指针,x,y
x7:正确,捕获 this 指针,并且可以修改对象内部变量的值

int main(void)
{
int a = 10, b = 20;
auto f1 = [] {return a; }; // error
auto f2 = [&] {return a++; }; // ok
auto f3 = [=] {return a; }; // ok
auto f4 = [=] {return a++; }; // error
auto f5 = [a] {return a + b; }; // error
auto f6 = [a, &b] {return a + (b++); }; // ok
auto f7 = [=, &b] {return a + (b++); }; // ok

return 0;
}

f1:错误,没有捕获外部变量,因此无法访问变量 a
f2:正确,使用引用的方式捕获外部变量,可读写
f3:正确,使用值拷贝的方式捕获外部变量,可读
f4:错误,使用值拷贝的方式捕获外部变量,可读不能写
f5:错误,使用拷贝的方式捕获了外部变量 a,没有捕获外部变量 b,因此无法访问变量 b
f6:正确,使用拷贝的方式捕获了外部变量 a,只读,使用引用的方式捕获外部变量 b,可读写
f7:正确,使用值拷贝的方式捕获所有外部变量以及 b 的引用,b 可读写,其他只读

在匿名函数内部,需要通过 lambda 表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下 lambda 表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。
返回值
很多时候,lambda 表达式的返回值是非常明显的,因此在 C++11 中允许省略 lambda 表达式的返回值。

// 完整的lambda表达式定义
auto f = [](int a) -> int
{
return a+10;
};

// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
return a+10;
};

constexpr

在 C++11 之前只有 const 关键字,从功能上来说这个关键字有双重语义:变量只读,修饰常量,举一个简单的例子:

void func(const int num)
{
const int count = 24;
int array[num]; // error,num是一个只读变量,不是常量
int array1[count]; // ok,count是一个常量

int a1 = 520;
int a2 = 250;
const int& b = a1;
b = a2; // error
a1 = 1314;
cout << "b: " << b << endl; // 输出结果为1314
}

在 C++11 中添加了一个新的关键字 constexpr,这个关键字是用来修饰常量表达式的
在定义常量时,const 和 constexpr 是等价的,都可以在程序的编译阶段计算出结果,例如:

const int m = f();  // 不是常量表达式,m的值只有在运行时才会获取。
const int i=520; // 是一个常量表达式
const int j=i+1; // 是一个常量表达式

constexpr int i=520; // 是一个常量表达式
constexpr int j=i+1; // 是一个常量表达式

对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。

// 此处的constexpr修饰是无效的
constexpr struct Test
{
int id;
int num;
};
struct Test
{
int id;
int num;
};

int main()
{
constexpr Test t{ 1, 2 };
constexpr int id = t.id;
constexpr int num = t.num;
// error,不能修改常量
t.num += 100;
cout << "id: " << id << ", num: " << num << endl;

return 0;
}

修饰函数
constexpr 并不能修改任意函数的返回值,时这些函数成为常量表达式函数,必须要满足以下几个条件:

  • 函数必须要有返回值,并且 return 返回的表达式必须是常量表达式。
// error,不是常量表达式函数
constexpr void func1()
{
int a = 100;
cout << "a: " << a << endl;
}

// error,不是常量表达式函数
constexpr int func1()
{
int a = 100;
return a;
}
函数 func1() 没有返回值,不满足常量表达式函数要求
函数 func2() 返回值不是常量表达式,不满足常量表达式函数要求
  • 函数在使用之前,必须有对应的定义语句。
#include <iostream>
using namespace std;

constexpr int func1();
int main()
{
constexpr int num = func1(); // error
return 0;
}

constexpr int func1()
{
constexpr int a = 100;
return a;
}
在测试程序 constexpr int num = func1(); 中,还没有定义 func1() 就直接调用了,应该将 func1() 函数的定义放到 main() 函数的上边。
  • 整个函数的函数体中,不能出现非常量表达式之外的语句(using 指令、typedef 语句以及 static_assert
    断言、return 语句除外)
// error
constexpr int func1()
{
constexpr int a = 100;
constexpr int b = 10;
for (int i = 0; i < b; ++i)
{
cout << "i: " << i << endl;
}
return a + b;
}

// ok
constexpr int func2()
{
using mytype = int;
constexpr mytype a = 100;
constexpr mytype b = 10;
constexpr mytype c = a * b;
return c - (a + b);
}

修饰模板函数
C++11 语法中,constexpr 可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数

#include <iostream>
using namespace std;

struct Person {
const char* name;
int age;
};

// 定义函数模板
template<typename T>
constexpr T dispaly(T t) {
return t;
}

int main()
{
struct Person p { "luffy", 19 };
//普通函数
struct Person ret = dispaly(p);
cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;

//常量表达式函数
constexpr int ret1 = dispaly(250);
cout << ret1 << endl;

constexpr struct Person p1 { "luffy", 19 };
constexpr struct Person p2 = dispaly(p1);
cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl;
return 0;
}
在上面示例程序中定义了一个函数模板 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

- struct Person ret = dispaly(p); 由于参数 p 是变量,所以实例化后的函数不是常量表达式函数,此时
constexpr 是无效的
- constexpr int ret1 = dispaly(250); 参数是常量,符合常量表达式函数的要求,此时 constexpr
是有效的
- constexpr struct Person p2 = dispaly(p1); 参数是常量,符合常量表达式函数的要求,此时
constexpr 是有效的

修饰构造函数

如果想用直接得到一个常量对象,也可以使用 constexpr 修饰一个构造函数,这样就可以得到一个常量构造函数了。常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值

#include <iostream>
using namespace std;

struct Person {
constexpr Person(const char* p, int age) :name(p), age(age)
{
}
const char* name;
int age;
};

int main()
{
constexpr struct Person p1("luffy", 19);
cout << "luffy's name: " << p1.name << ", age: " << p1.age << endl;
return 0;
}

委托构造函数

委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。下面举例说明:

#include <iostream>
using namespace std;

class Test
{
public:
Test() {};
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}

Test(int max, int min)
{
this->m_max = max > 0 ? max : 100; // 冗余代码
this->m_min = min > 0 && min < max ? min : 1;
}

Test(int max, int min, int mid)
{
this->m_max = max > 0 ? max : 100; // 冗余代码
this->m_min = min > 0 && min < max ? min : 1; // 冗余代码
this->m_middle = mid < max && mid > min ? mid : 50;
}

int m_min;
int m_max;
int m_middle;
};

int main()
{
Test t(90, 30, 60);
cout << "min: " << t.m_min << ", middle: "
<< t.m_middle << ", max: " << t.m_max << endl;
return 0;
}

在上面的程序中有三个构造函数,但是这三个函数中都有重复的代码,在 C++11 之前构造函数是不能调用构造函数的,加入了委托构造之后,我们就可以轻松地完成代码的优化了:

#include <iostream>
using namespace std;

class Test
{
public:
Test() {};
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}

Test(int max, int min):Test(max)
{
this->m_min = min > 0 && min < max ? min : 1;
}

Test(int max, int min, int mid):Test(max, min)
{
this->m_middle = mid < max && mid > min ? mid : 50;
}

int m_min;
int m_max;
int m_middle;
};

int main()
{
Test t(90, 30, 60);
cout << "min: " << t.m_min << ", middle: "
<< t.m_middle << ", max: " << t.m_max << endl;
return 0;
}

在修改之后的代码中可以看到,重复的代码全部没有了,并且在一个构造函数中调用了其他的构造函数用于相关数据的初始化,相当于是一个链式调用。在使用委托构造函数的时候还需要注意一些几个问题:

  • 这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
  • 如果要进行多层构造函数的链式调用,建议将构造函数的调用的写在初始列表中而不是函数体内部,否则编译器会提示形参的重复定义。

继承构造函数

C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。先来看没有继承构造函数之前的处理方式:

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

int m_i;
double m_j;
string m_k;
};

class Child : public Base
{
public:
Child(int i) :Base(i) {}
Child(int i, double j) :Base(i, j) {}
Child(int i, double j, string k) :Base(i, j, k) {}
};

int main()
{
Child c(520, 13.14, "i love you");
cout << "int: " << c.m_i << ", double: "
<< c.m_j << ", string: " << c.m_k << endl;
return 0;
}

继承构造函数的使用方法是这样的:通过使用 using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

int m_i;
double m_j;
string m_k;
};

class Child : public Base
{
public:
using Base::Base;
};

int main()
{
Child c1(520, 13.14);
cout << "int: " << c1.m_i << ", double: " << c1.m_j << endl;
Child c2(520, 13.14, "i love you");
cout << "int: " << c2.m_i << ", double: "
<< c2.m_j << ", string: " << c2.m_k << endl;
return 0;
}

在修改之后的子类中,没有添加任何构造函数,而是添加了 using Base::Base; 这样就可以在子类中直接继承父类的所有的构造函数,通过他们去构造子类对象了。
另外如果在子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数:

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

void func(int i)
{
cout << "base class: i = " << i << endl;
}

void func(int i, string str)
{
cout << "base class: i = " << i << ", str = " << str << endl;
}

int m_i;
double m_j;
string m_k;
};

class Child : public Base
{
public:
using Base::Base;
using Base::func;
void func()
{
cout << "child class: i'am luffy!!!" << endl;
}
};

int main()
{
Child c(250);
c.func();
c.func(19);
c.func(19, "luffy");
return 0;
}