C++11 学习笔记
上篇文章《理解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标准出现的时候