C++11 学习笔记

  • 2017-06-08
  • 15,612
  • 2

上篇文章《理解JNI技术》介绍了Java与C/C++代码交互的一项技术。由于在项目中有用到C++代码,所以重拾吃灰半年多的《C++ Primer 第5版》。大学期间也学过Hello World版的C++,由于C++11版标准变化之大(据悉ART虚拟机的实现代码已经切换到了C++11了),完全不是我以前认识的那个C++了,所以专门记录一下,并且会不定期更新,最近很火的Kotlin之后也会写一篇学习笔记。

大多数Android开发者平时很少接触native代码,但是Android源代码中有绝大部分代码是用C/C++实现的,如果想深入Android底层,至少应该能看懂。当然如果对自己要求更高的话,可以使用native代码结合JNI技术重构追求性能,对安全性要求高或者计算密集型的模块。本篇文章会通过类比Java语法来便于读者理解。

以下下代码片段已经上传至GitHub,地址在:https://github.com/pqpo/CppPrimer

运行方式:

通过命令行 “g++ -std=c++11 a.cpp -o a” 进行编译,在Linux系统下生成a文件,Windows系统下生成a.exe文件,命令行输入a即可运行。

# 变量和基本类型

C++中的算术类型基本和Java保持一致,主要区别在于Java严格规定了每一种数据类型的所需字节数,由于C++和硬件平台关联较大,所以它只定义了每种数据类型最少需要多少字节。可以通过sizeof操作符查看某变量所占字节数:

#include<iostream>

using namespace std;

int main(int argsSize, char* args[]) {
bool aBool = true;
cout << "bool size:" << sizeof(aBool) << endl;
char aChar = 'a';
cout << "char size:" << sizeof(aChar) << endl;
short aShort = 0xee;
cout << "short size:" << sizeof(aShort) << endl;
int aInt = 10;
cout << "int size:" << sizeof(aInt) << endl;
long aLong = 10L;
cout << "long size:" << sizeof(aLong) << endl;
long long aLongLong = 10L;
cout << "long long size:" << sizeof(aLongLong) << endl;
float aFloat = 10.0f;
cout << "long size:" << sizeof(aLong) << endl;
double aDouble = 10.0;
cout << "double size:" << sizeof(aDouble) << endl;
long double aLongDouble = 10.0;
cout << "long double size:" << sizeof(aLongDouble) << endl;
}

其中bool类型最小尺寸未定义,char字符类型最小尺寸为8位,int最小尺寸16位,float最小尺寸为6位有效数字,double为10位有效数字。

另外还可以使用unsigned关键字定义除bool和浮点数外的无符号数。

## 变量初始化

变量的初始化与Java类似,但是不同的是:
在Java中,类的成员变量会自动初始化为0值,函数的临时变量必须显式初始化。在C++中,定义于任何函数体之外的内置类型变量会被初始化为0,定义在函数体内部的内置类型将不被初始化。对于C++对象无需使用new关键字即可初始化对象,绝大多数类支持无需显式初始化而定义对象,是否允许这种行为由类觉得,并且决定了初始值。


#include <iostream>

using namespace std;

//执行结果:
//global_str: global_int:0
//local_str: local_str2:Hello local_int:4200843

string global_str; //非显式地初始化为一个空字符串
int global_int; //函数体外的内置类型初始化为0

int main(int argsSize, char* args[]) {
	string local_str; //非显式地初始化为一个空字符串(不管在函数体内还是外)
	string local_str2("Hello"); //显式地初始化字符串
	int local_int;//函数体内的内置类型不初始化,变量的值是未定义,不可预计,避免不初始化
	cout << "global_str:" << global_str << " global_int:" << global_int << endl;
	cout << "local_str:" << local_str << " local_str2:" << local_str2 << " local_int:" << local_int << endl;
	return 0;
}

## 变量声明与定义


extern int i; //声明i
int j; // 声明并定义j
extern double pi = 3.14 //定义

变量的声明一般出现在头文件中,其中函数的声明可以隐藏extern。
变量可以声明多次,能且只能定义一次。

##  复合类型

在Java中赋值操作(=)左值一般是一个引用,内置类型为赋值, 函数参数传递也类似,内置类型以值传递,比如int, boolean, float等;类对象以引用传递。

然而在C++中默认为值传递,赋值操作其实是一次拷贝(创建一个副本),即使是一个对象传递给函数会创建一个以形参为名的副本。所以引入了两种复合类型:引用和指针。

在函数的形参中声明引用或者指针可以避免拷贝,其中引用是C++11中新增的,推荐使用。

引用就是给对象起一个别名,通过&来定义引用类型,并且必须被初始化。

下面是分别使用引用和指针实现交换变量的例子:


#include <iostream>

using namespace std;

void swap1(int &x, int &y);
void swap2(int *x, int *y);

int main(int arg, char* args[]) {
	int x = 10;
	int y = 20;
	swap1(x, y);
	cout << "x:" << x << " y:" << y << endl; //x:20 y:10
	swap2(&x, &y);
	cout << "x:" << x << " y:" << y << endl; //x:10 y:20
}

void swap1(int &x, int &y) {
	int tmp = x;
	x = y;
	y = tmp;
}

void swap2(int *x, int *y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

指针的赋值需要使用取地址符(&)来拿到变量的地址,事实上,指针储存的变量就是一个内存地址,可以赋值为nullptr,代表空指针(值为0),使用解引用符(*)取得指针指向的值。而引用可以直接赋值,并且必须赋值,不能定义引用的引用,必须绑定在一个对象上。

这样看来Java中的引用更像是C++中的指针,存在所谓的空指针异常。

另外还有一个比较特殊的变量指针void*,它是一种特殊的指针类型,可用于存放任意对象地址。一个void*指针存放一个地址,但是我们不知道该地址到底是什么类型的对象。

除了指向变量的指针,在C++中还存在指向函数的指针:函数指针。比如:


#include <iostream>

using namespace std;

void swap(int &x, int &y);

int main(int arg, char* args[]) {
	int x = 10;
	int y = 20;
	
	void (*methodPtr)(int &x, int &y);//声明一个函数指针
	methodPtr = &swap; //函数指针赋值
	methodPtr = swap;//取地址符可省略,效果和上面一致
	methodPtr(x, y); //像给函数起了一个别名,可以直接使用()调用
	cout << "x:" << x << " y:" << y << endl; //x:20 y:10
}

void swap(int &x, int &y) {
	int tmp = x;
	x = y;
	y = tmp;
}

同样的,函数指针也可以作为参数传入另一个函数:


#include <iostream>
#include <vector>

using namespace std;

void mMap(vector list, void (*fun)(int item));
void log(int item);

int main(int arg, char* args[]) {
	vector list = {2,3,4,3,2,1,2};
	mMap(list, &log);
	//mMap(list, log);
}

void log(int item) {
	cout << item << endl; 
}

void mMap(vector list, void (*fun)(int item)) {
	for(int it : list) {
		fun(it);
	}
}

函数名对应的就是函数指针的值,故可以直接传入函数名,但是函数签名必须一致。更常用的是传入lambda表达式。关于lambda表达式后面会提到。

## const 限定符

类似Java中的final关键字,代表它的值是不能被改变的。

默认状态下,const对象仅在文件内有效,如果 想在多个文件中共享const对象,必须在变量的定义之前添加extern关键字。

需要特别注意的是指向常量的指针(pointer to const),和常量指针(const pointer):


#include<iostream>

using namespace std;

int main(int arg, char* args[]) {
	
	int i = 100;
	const int *ptrI = &i;//指向常量的指针,底层const
	// *ptrI = 200; 不可改变该常量的值
	int j = 200;
	ptrI = &j; //指针本身非常量,可以指向其他对象
	
	int x = 300;
	int *const ptrX = &x; //一个常量指针,顶层const
	*ptrX = 400; //可以改变对象的值
	//ptrX = &j; //但不许指向其他对象

        const int *const ptrY = &x; //甚至可以这样定义,既是顶层const也是底层const
        const int ci = 42; //顶层const
        const int &r = ci; //用于声明引用的const都是底层const


}

用名称顶层 const 表示指针本身是常量,用名称底层 const 表示指针所指的对象是常量。

## auto类型说明符

auto让编译器通过初始值来推算变量的类型,auto定义的变量必须有初始值。


int val1 = 1;
int val2 = 2;
auto item = val1 + val2;

item的类型自动推断出为 int。

## decltype类型指示符


decltype(f()) sum = x;//sum类型就是函数f的返回类型

编译器并不实际调用f。


int i = 1;
decltype(i) d;//d的类型为int,未初始化
decltype((i)) e = i;//e的类型为int引用,必须初始化

# 数组

C++11中提供了使用更方便的verctor类型,类似Java的List,有兴趣的可以学习一下。
数组的定义:


int main(int arg, char* args[]) {
	char a1[] = {'a', 'b', 'c', 'd'}; // 定义并初始化一个char数组
	char (*a2)[4] = &a1; //a2是一个指针,指向长度为4的char数组
	char (&a3)[4] = *a2; //a3是一个引用,引用指针a2的内容,即a1
	char a4[] = {'e', 'f', 'g'};
	char* a5[] = {a1, a4}; //a5是一个数组,数组内容是指向char的指针(a1, a4声明为数组,也表示为指向首元素的指针)
}

## 数组访问


cout << a1[3] << endl;
cout << *(a1 + 3) << endl;

支持和Java中一致的下标访问,由于在C++语言中,指针和数组有非常紧密的联系,使用数组的时候编译器一帮会把它转换成指针,所以也可以通过移动指针,然后解引用来进行数组访问。

## 数组遍历


int main(int arg, char* args[]) {
	char a1[] = {'a', 'b', 'c', 'd'}; // 定义并初始化一个char数组	
	for(auto item : a1) {
		cout << item << endl;
	}	
	for(int i = 0; i < 4; ++i) {
		cout << *(a1 + i) << endl;
               // cout << a1[i] << endl;
	}	
	char* e = a1 + 4; //尾元素下一个位置
	for(char* b = a1; b != e; ++b) {
		cout << *b << endl;
	}	
	char* pbeg = a1;//或者 begin(a1)
	char* pend = end(a1);//使用标准库函数获取尾元素的下一个位置
	while(pbeg != pend) {
		cout << *pbeg++ << endl;
	}
}

遍历方式有很多种,可以使用类似Java的foreach和下标访问,还可以使用指针访问,使用起来比Java更加灵活也更容易出错。

# 类型转换

C++中也有Java中的隐式转换,比如这样:


float fval = 3.14f;
int ival = fval;

上述情况在Java中需要强制类型转换,C++中编译器可能会警告。Java中的隐式转换一般都适用于C++中。
除此之外还有bool类型的转换,函数指针的转换,数组到指针的转换等。
当然也有显示转换,在Java中一般这样做显示转换:


float fval = 3.14f;
int ival = (float)fval;

在C++中同样适用,但是在C++11中提供了更安全的转换方式:
static_cast, dynamic_cast, const_cast, reinterpret_cast。
比如:


double d = 1.0;
void *p = &d;
double *dp = static_cast<double*>(p);
	
const char *pc;
char *p = const_cast<char*>(pc);//此时虽然p指针不是const,但是通过p写值是未定义的
	
int *ip;
char *pc = reinterpret_cast<char*>(ip);//很容易出现错误

具体使用场景可以通过上述例子体会体会。

# lambda 表达式

具体形式如下:


[capture list](parameter list) -> return type { function body };

举个栗子:


void mMap(vector list, void (*fun)(int item));

int main(int arg, char* args[]) {
	vector list = {2,3,4,3,2,1,2};
	mMap(list, [](int item) -> void { cout << item << endl; });
}

void mMap(vector list, void (*fun)(int item)) {
	for(int it : list) {
		fun(it);
	}
}

将vector中的的每个元素都执行一遍lambda表达式。
还可以这样:


function<int()> addLater(int a, int b) {
	return [a, b]()->int{ return a + b; };
}

int add(int a, int b) {
	return a + b;
}

int (*fun(int a))(int, int) {
	cout << a << endl;
	return add;
}

int main(int arg, char* args[]) {
	function<int()> addResult = addLater(1, 2);
	cout << addResult() << endl;
	
	int (*addFunc)(int, int);
	addFunc = fun(10)
	cout << addFunc(1, 10) << endl;
}

通过函数返回lambda表达式实现了延迟计算的目的,这里用到了捕获列表,也就是方括号“[a, b]”,这样在函数体内就用使用外部函数的局部变量了。
另外还可以看出lambda表达式实际上是一个函数对象,用function<int()>表示,泛型正式lambda表达式的函数签名。

# 动态内存与智能指针

Java中的内存是由JVM全权掌握的,一般情况下我们不必关心内存的申请和释放。
在C++中,代码块内的局部变量如果不使用new关键字初始化对象,会在栈内存中分配内存,代码块结束之后自动释放内存。
如果使用new关键字初始化对象则会在堆中申请内存,返回一个指针,代码块结束之后对象保留,我们需要在合适的时机使用delete关键字释放内存,
这个合适的时机非常不容易把握,所以引入了智能指针: shared_ptr,unique_ptr,weak_ptr

  • shared_ptr是以引用计数的方式管理指针的,引用为0时释放内存,会存在循环引用无法释放的情况。
  • unique_ptr在同一时刻只能有一个unique_ptr指向一个给定对象。
  • weak_ptr与shared_ptr配合使用不印象shared_ptr的指针计数,可以用于解决循环引用的问题。

下面是一些例子:


#include <iostream>
#include <memory>

using namespace std;

class Foo{
	public:
		Foo() {
			cout << "default constructor" << endl;
		}
		~Foo() {
			cout << "destructor" << endl;
		}
		void print() {
			cout << "print" << endl;
		}
};

void foo1() {
	Foo f; // 局部变量在栈中分配内存,自动析构,所以不能返回给调用者
}

Foo* foo2() {
	return new Foo(); // 使用new关键字在堆中分配内存,可以返回给调用者,使用完毕之后需要及时delete,防止内存泄漏
}

shared_ptr foo3() {
	auto ptr = make_shared(); //引用计数为1
	return ptr; //函数返回试一次拷贝,引用计数加1
} //函数出栈引用计数减1

int main(int arg, char* args[]) {
	
	foo1();
	
	Foo *f = foo2();
	delete f; //不容易管理
	
	//使用智能指针,引用计数
	auto fPtr = foo3();
	
	fPtr -> print();//使用智能指针,和普通指针一致
	
	auto fPtr2(fPtr);//引用计数加1
	
	cout << "count:" << fPtr.use_count() << endl; //共享的指针数量
	fPtr2.reset();//引用计数减1
	cout << "count:" << fPtr.use_count() << endl;
	fPtr.reset();//引用计数减1,此时引用计数为0,执行析构函数
	cout << "count:" << fPtr.use_count() << endl;
	
	//同一时刻只能有一个unique_ptr指向一个给定对象
	unique_ptr uptr1(new Foo);
	unique_ptr uptr2;
	uptr2.reset(uptr1.release());//转移指针所有权
	uptr2.reset();//释放对象
	
}

# 类

## 类的构造函数与重载运算符

在C++11中类的构造函数比Java更加灵活,而且还支持运算符的重载,所谓运算符重载就是支持类似两个对象相加等操作,比较典型的是Java中两个字符串的相加就是一个重载运算符的例子。另外需要注意的是析构函数,在销毁一个对象的时候将会被调用。


class Foo {
	public:
		string a;
		int b;
		Foo() { //默认构造函数
			cout << "default constuctor" << endl;
		}
		Foo(string x): a(x), b(0){ //普通构造函数
			cout << "general constructor 1" << endl;
		}
		Foo(string x, int y): a(x),b(y){ //普通构造函数
			cout << "general constructor 2" << endl;
		}
		Foo(const Foo &orig):a(orig.a),b(orig.b) { //拷贝构造函数
			cout << "copy consrtuctor" << endl;
		}
		~Foo(){ //析构函数
			cout << "destructor" << endl;
		} 
		Foo& operator=(const Foo &other) { //拷贝赋值表达式
			a = other.a;
			b = other.b;
			cout << "copy-assignment operator" << endl;
			return *this;
		}
		void operator()() { //函数调用运算符
			cout << "a:" << a << " b:" << b << endl;
		}
};

Foo operator+(const Foo &l, const Foo &r) { //非成员运算符函数
	Foo sum = l;
	sum.a += r.a;
	sum.b += r.b;
	return sum;
}

int main(int arg, char* args[]) {
	Foo a; //调用默认构造函数
	Foo b = a; //调用拷贝构造函数,不完全等价于 Foo b,b.operator=(a);
	Foo c("xxx", 2.1);
	c = a; //调用拷贝赋值表达式
	string param = "yyyy";
	Foo d = param; //隐式调用构造函数Foo(string x)
	
	Foo r("r", 2);
	Foo l("l", 3);
	Foo sum =  r + l;//调用非成员运算符函数,等价于 sum = operator+(r, l);
	cout << sum.b << endl;
	sum(); //调用函数调用运算符,等价于 sum.operator()();
}

## 友元

我们不能在一个类的外部访问一个类的私有成员,为了方便C++中引入了友元的概念,声明了友元的类或者类方法有访问被类私有成员的权限,这个属性不具备继承性,如果父类是某个类的友元,子类不会继承改属性,需要重新声明。
下面是友元的例子:


class Foo;

class Boo {
	public:
		void printName(Foo &foo);
};
class Foo {
	friend void Boo::printName(Foo &foo); // 声明友元,代表这个方法可以范围我的私有成员,也可以写类名,表示这个类的的所有方法都可以范围。
	public: 
		Foo(string name);
	private:	
		string name;
};

void Boo::printName(Foo &foo) {
	cout << foo.name << endl;
}

Foo::Foo(string name):name(name) {}

int main(int arg, char* args[]) {
	Foo foo("a");
	Boo boo;
	boo.printName(foo);
}

上面的例子中Boo类的printName方法可以访问Foo类的私有成员name。

## 继承、虚函数与多态

在c++中像下面这样定义基类与派生类:


class Base {
	
	public:
		Base(string name, int age):name(name),age(age){}
		void say() {
			cout << "Base say: my name is " <<  name << endl;
		}
	protected:
		string name;
		int age;
	
	
};

class Son: public Base {
	
	public:
		Son(string name, int age):Base(name,age){}
		void say() {
			cout << "Son say: my name is " <<  name << endl;
		}
	
};

int main(int arg, char* args[]) {
	Son son("Tom", 12);
	Base& base = son;
	base.say();
}

对于学过Java的应该很清楚main函数所打印的值应该是“Son say: my name is Tom”,这正是多态的表现,基于方法访问的动态绑定。
运行一下事实并非如此,打印结果为:“Base say: my name is Tom”,难道C++不支持多态吗?非也,只要将基类的say函数声明为虚函数即可:


class Base {
	
	public:
		Base(string name, int age):name(name),age(age){}
		virtual void say() {
			cout << "Base say: my name is " <<  name << endl;
		}
	protected:
		string name;
		int age;
	
	
};

class Son: public Base {
	
	public:
		Son(string name, int age):Base(name,age){}
		void say() override {
			cout << "Son say: my name is " << name << endl;
		}
	
};

int main(int arg, char* args[]) {
	Son son("Tom", 12);
	Base& base = son;
	base.say();
        base.Base::say();
}

在函数前加上virtual关键字,这个函数即被声明为虚函数,而且一旦基类函数声明为了虚函数,起子类不管有没有声明virtual均为虚函数。再看子类中的say函数最后加了关键字override,这个是可选的,为了防止方法名写错,在编译器层面保证了这次覆写是有效的,类似于Java中的注解@override。这样执行结果就提现了多态性。如果想回避虚函数的动态绑定,使用作用域运算符可以强制调用父类的方法。
还有一种函数叫做纯虚函数,类比于Java的抽象函数(abstract ),含有纯虚函数的类是抽象基类,不能直接创建一个抽象基类对象,必须由子类覆写纯虚函数。 只要在定义虚函数的后面加上“=0”,即可将该函数定义为纯虚函数,例如:


class Base {
	
	public:
		Base(string name, int age):name(name),age(age){}
		virtual void say() = 0;
	protected:
		string name;
		int age;
	
};

class Son: public Base {
	
	public:
		Son(string name, int age):Base(name,age){}
		void say() override {
			cout << "Son say: my name is " << name << endl;
		}
	
};

int main(int arg, char* args[]) {
	Son son("Tom", 12);
	Base& base = son;
	base.say();
}

 

## 模板与泛型

Java中的泛型是JDK 1.5的一项新特性,给我们的编程提供了极大的便利和安全性,事实上Java的泛型只是一个语法糖,在编译时会进行泛型擦除,而且Java的泛型正是起源于C++模板(Templates),所以我们可以用Java泛型来类比C++模板,C++的模板比Java泛型更加灵活,使用上也需要更加小心。
下面是一个泛型函数的例子:


template 
int compare(const T &v1, const T &v2) {
	if (v1 < v2) {
		return -1;
	}
	if (v1 > v2) {
		return 1;
	}
	return 0;
}

int main(int arg, char* args[]) {
	cout << compare(1, 2) << endl;
}

上面的例子中T被自动推断成了int,此时compare函数等价于:


int compare(const int &v1, const int &v2) {
	if (v1 < v2) {
		return -1;
	}
	if (v1 > v2) {
		return 1;
	}
	return 0;
}

即把所有出现T的地方用int代替即可。
除了模板函数,也可以定义模板类,只需要将类中出现的模板类型的地方用指定的类型替换了即可,下面是模板类的例子:


template  class Plus {
	public:
		Plus(const T a,const T b):a(a),b(b){}
		T getResult() {
			return a + b;
		}
	
	private:
		T a;
		T b;
	
};

int main(int arg, char* args[]) {
	Plus intPlus(1,2);
	cout << intPlus.getResult() << endl; //输出3
	string a("Hello ");
	string b("C++");
	Plus stringPlus(a, b);
	cout << stringPlus.getResult() << endl; //输出 Hello C++
}

在C++标准模板库中有很多常用的类或函数都是使用模板定义的,例如vector,set,map,move等。

>> 转载请注明来源:C++11 学习笔记

评论

  • fd5788回复

    引用在C++中早就存在的

    • pqpo回复

      早在C++11标准出现的时候

回复给 pqpo 点击这里取消回复。