本文算做是 《Android 音视频开发打怪升级》系列文章的“番外”篇,本来打算将本文的内容写在 《Android FFmpeg视频解码播放》 这篇文章中,由于要想学习 FFmpeg
相关知识,C++
的基础知识是必不可少的。html
可是写着写着发现,篇幅仍是太长了,加上有部分小伙伴对 C++
可能也比较熟悉,因此把此节独立成篇,更有利于不熟悉 C++
的小伙伴学习查看,熟悉的小伙伴也能够直接跳过。java
C++
相对于 Java
仍是有许多的不一样之处,对于没有使用过 C++
的人来讲,若是要学习 NDK
开发,C++
是第一道坎,必需要掌握。程序员
本文经过对比的方式,把 C++
和 Java
之间最基础,也是最常使用知识的异同标记出来,方便你们学习。bash
固然了,本文只是重点对 C++
中最经常使用的,也是重点的知识进行讲解,若有时间,最好仍是系统地学一下相关的基础知识。函数
本文使用对比的方式,将
C++
与咱们很是熟悉的Java
进行对比学习,介绍C++
与Java
使用的异同,帮助你们快速入门C++
。post
C++ 提供了一下几种基础数据类型学习
类型 | 关键字 |
---|---|
布尔型 | bool |
字符型 | char |
整型 | int |
浮点型 | float |
双浮点型 | double |
无类型 | void |
同时,这些类型还能够被类型修饰符修饰,拓展出更多的数据类型:网站
类型修饰符 | 关键字 |
---|---|
有符号类型 | signed |
无符号类型 | unsigned |
短类型 | short |
长类型 | long |
其中 signed
和 unsigned
指定了数据是否有正负; short
和 long
主要指定了数据的内存大小。ui
因为不一样的系统,同个数据类型所占用的内存大小也不必定是同样的,如下是典型值:this
类型 | 内存大小 | 范围 |
---|---|---|
char | 1 个字节 | -128到127 或 0到255 |
unsigned char | 1 个字节 | 0 到 255 |
signed char | 1 个字节 | -128 到 127 |
int | 4 个字节 | -2147483648 到 2147483647 |
unsigned int | 4 个字节 | 0 到 4294967295 |
signed int | 4 个字节 | -2147483648 到 2147483647 |
short int | 2 个字节 | -32768 到 32767 |
unsigned short int | 2 个字节 | 0 到 65,535 |
signed short int | 2 个字节 | -32768 到 32767 |
long int | 8 个字节 | -xxx 到 xxx |
signed long int | 8 个字节 | -xxx 到 xxx |
unsigned long int | 8 个字节 | -xxx 到 xxx |
float | 4 个字节 | -xxx 到 xxx |
double | 8 个字节 | -xxx 到 xxx |
long double | 16 个字节 | -xxx 到 xxx |
能够看到,
short
修饰符将原类型内存大小减少一半;
long
修饰符将原数据类型内存大小扩大一倍。
C++
是一门面向对象的语言,类是必不可少的。其类的定义与 Java
大同小异。
Java
类一般声明和定义一般都是在同一个文件 xxx.java
中。
而 C++
类的声明和定义一般是分开在两个不一样的文件中,分别是 .h 头文件
和 .cpp 文件
一个 类的头文件
一般以下:
// A.h
class A {
private: //私有属性
int a;
void f1();
protected: //子类可见
int b;
void f2(int i);
public: //公开属性
int c = 2;
int f3(int j);
A(int a, int b); // 构造函数
~A(); //析构函数
};
复制代码
对应的类实现文件 A.cpp
以下:
// A.cpp
/** * 实现构造函数 */
A::A(int a, int b):
a(a),
b(b) {
}
// 等价于
/* A::A(int a, int b) { this.a = a; this.b = b; } */
/** * 实现析构函数 */
A::~A() {
}
/** * 实现 f1 方法 */
void A::f1() {
}
/** * 实现 f2 方法 */
void A::f2(int j) {
this.b = j
}
/** * 实现 f3 方法 */
int A::f3(int j) {
this.c = j
}
复制代码
能够看到,.h
文件主要负责类成员变量和方法的声明; .cpp
文件主要负责成员变量和方法的定义。
可是,并不是必定要按照这样的结构去实现类,你也能够在 .h
头文件中直接定义变量和方法。
好比:
// A.h
class A {
private:
int a = 1;
public:
void f1(int i) {
this.a = i;
}
}
复制代码
1) 可见性 private、protected、public
这几个关键字和 Java
是同样的,只不过在 C++
中,一般不会对每一个成员变量和方法进行可见性声明,而是将不一样的可见性的变量和方法集中在一块儿,统一声明,具体见上面定义的类A。
2) 构造函数和析构函数
C++
中类的构造函数和 Java
基本一致,只不过,在实现构造函数时,对成员变量的初始化方式比较特别。以下:
A::A(int a, int b):
a(a),
b(b) {
}
// 等价于
A::A(int a, int b) {
this.a = a;
this.b = b;
}
复制代码
以上两种方式均可以,一般使用第一种方式。
析构函数
则是 Java
中没有的。经过波浪符号 ~
进行标记。
它和构造函数同样,都是由系统自动调用,只不过,构造函数
在类建立的时候调用,析构函数
在类被删除的时候调用,主要用于释放内部变量和内存。
析构函数的声明形式为 ~类名();
实现的形式为 类名::~类名() { }
具体见上面类 A 的写法。
3) ::
双冒号
看了上面类的定义,确定会对 ::
这个符号感到很神奇。这是 C++
中的 域做用符
,用于标示变量和方法是属于哪一个域的,好比上面的
void A::a() { }
复制代码
说明 方法a
是属于 类A
的。
也能够用于调用类的静态成员变量,如
//A.h
class A {
private:
static int a = 1;
int b;
void a();
}
//A.cpp
void A::a() {
b = A::a;
}
复制代码
C++
类的继承和 Java
也是大同小异,其格式以下:
class B: access-specifier A
,其中 access-specifier
是访问修饰符, 是 public
、protected
或 private
其中的一个。
访问修饰符的做用以下:
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,可是能够经过调用基类的公有和保护成员来访问。
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
一般状况下,咱们都是使用
公有继承(public)
,也就是和Java
是同样的。
Java
中,子类只能继承一个父类,可是 C++
能够继承自多个父类,使用逗号 ,
隔开:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
复制代码
Java
中,是没有指针的概念的,可是其实 Java
中除了基本数据类,大部分状况下使用都是 指针
。
好比下面这段 Java
代码:
People p1 = new People("David","0001");
People p2 = p1;
p2.setName("Denny");
System.out.println(p1.getName());
// 输出结果为:Denny
复制代码
缘由就是 p1 和 p2 都是对对象的引用,在完成赋值语句 People p2 = p1; 后, p2 和 p1 指向同一个存储空间,因此对于p2的修改也影响到了p1。
那么,为何在 Java
中不多去关注指针呢?
由于 Java
已经将指针封装了,也不容许显式地去操做指针,而且 Java
中的内存都由虚拟机进行管理,无需咱们去释放申请的内存。
1) 指针的声明和定义
与 Java
不一样的是,C++
中的指针概念很是重要,而且无处不在。
指针
:是一个变量,这个变量的值是另外一个变量的内存地址。也就是说,指针是一个指向内存地址的变量。
指针的声明和定义方法以下:
int a = 1; // 实际变量的声明
int *p; // 指针变量的声明
p = &a; // 指针指向 a 的内存地址
printf("p 指向的地址: %d, p指向的地址存储的内容: %d\n", p, *p);
// 输出以下:
// p 指向的地址: -1730170860, p指向的地址存储的内容: 1
复制代码
这个例子中有两个很重要的符号: *
、&
。其中:
*
:有两个做用:
i. 用于定义一个指针: type *var_name;
,var_name
是一个指针变量,如 int *p;
ii. 用于对一个指针取内容: *var_name
, 如 *p
的值是 1
。
&
:是一个取址符号
其用于获取一个变量所在的内存地址。如 &a;
的值是 a
所在内存的位置,即 a
的地址。
经过上面的例子,可能没法很好的理解指针的用处,来看另外一个例子。
class A {
public:
int i;
};
int main() {
//-----1-------
A a = A(); // 定变量 a
a.i = 1; // 修改 a 中的变量
A b = a; // 定义变量 b ,赋值为 a
A *c = &a; // 定义指针 c,指向 a
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 输出:1, 1, 1
//-----2-------
b.i = 2; //修改 b 中的变量
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 输出:1, 2, 1
//-----3-------
c->i = 3; //修改 c 中的变量
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 输出:3, 2, 3
//-----4-------
// 打印地址
printf("%d, %d, %d\n", &a, &b, c);
// 输出:-1861360224, -1861360208, -1861360224
return 0;
}
复制代码
上面的例子,定义了一个变量 a
,而后将 a
分别赋值给普通变量 b
和指针变量 c
。
第一次,打印三个变量中的成员变量的 i
的值都为 1
;
第二次,修改了 b
中的 i
,结果只修改了 b
的值,对 a
和 c
都没有影响;
第三次,修改了 c
中的 i
,结果修改了 a
和 c
的值,对 b
都没有影响;
最后,打印了三个变量的地址,能够发现 a
和 c
的值是同样的,b
的地址不同。
从这个例子就能够看出端倪了:
经过
普通变量
赋值的时候,系统建立了一个新的独立的内存块,如b
,对b
的修改,只影响其自己;
经过
指针变量
赋值时,系统没有建立新的内存块,而是将指针指向了已存在的内存块,如c
, 任何对c
的修改,都将影响原来的变量,如a
。
还有一点须要注意的是,指针变量
对成员变量的引用,使用的是箭头符号 ->
,如 c->i
;普通变量对成员变量的引用,使用的是点符号 .
,如 b.i
。
2) new 和 delete
在上面的例子中,是经过建立了一个变量 a
,而后将 指针变量 c
指向了 a
的方式定义了 c
。还有另一种方法,能够声明和定义一个指针变量,那就是经过 new
动态建立。
class A {
public:
int i;
}
int main() {
A *a = new A();
a->i = 0;
printf("%d\n", a->i);
// 输出: 0
// 删除指针变量,回收内存
delete a;
return 0;
}
复制代码
这就是动态建立指针变量的方式,这是 C++
经常使用的方式。
重要提醒:
要注意的是,经过
new
的方式建立的指针变量和不经过new
建立的变量最大的区别在于:经过new
建立的指针须要咱们本身手动回收内存,不然将会致使内存泄漏。回收内存则是经过delete
关键字进行的。
也就是说,
new
和delete
必需要成对调用。
int main() {
A a = A(); // 无new,main 函数结束后,系统会自动回收内存
A *b = new A(); // new 方式建立,系统不会自动回收内存,要手动 delete
delete b; // 手动删除,回收内存
return 0;
}
复制代码
能够看到,C++
的指针变量其实更接近与 Java
中普通变量的使用方式。
引用
是除了指针外,另外一个很是重要的概念。在 C++
也是常用的。
引用指的是:为一个变量起一个别名,也就是说,它是某个已存在变量的另外一个名字。
引用和指针很是的类似,初学者很是容易把这二者混淆了。
首先来看下如何声明一个引用变量。
// 声明一个普通变量 i
int i = 0;
// 声明定义一个引用 j
int &j = i;
j = 1;
printf("%d, %d\n", i, j)
// 输出:1, 1
复制代码
是否是有点熟悉,又是与符号 &
,可是这里并不是表示取址,这里只是做为一个标示符号。
请记住,千万不要和取址符号混淆,取址表示方式是:A *p = &a;
在上面的例子中,修改了 j
的值,i
的值也发生了变化。这和指针是否是很是像?
那么,引用和指针有什么不同呢?
i. 不存在空引用。引用必须链接到一块合法的内存。
ii. 一旦引用被初始化为一个对象,就不能被指向到另外一个对象。指针能够在任什么时候候指向到另外一个对象。
iii. 引用必须在建立时被初始化。指针能够在任什么时候间被初始化。
i 和 iii 都很好理解,就是声明引用的时候,必需要初始化好,而且不能初始化为空 NULL
。
ii 是最让人不理解的,什么叫作 “不能被指向到另外一个对象” ?
看如下的例子:
int i = 0;
// 定义引用 j ,指向 i
int &j = i;
int k = 1;
// 这个操做是指向另一个对象吗?
j = k;
printf("%d, %d, %d\n", i, j, k);
// 输出:1, 1, 1
// 打印地址
printf("%d, %d, %d\n", &i, &j, &k);
// 输出:-977299952, -977299952, -977299948
复制代码
能够看到,i
j
k
三个的值都变成了 1
,这看起来和指针是同样的效果,但却有质的区别。
看最后一个打印输出,i
和 j
的地址始终是同样的,和 k
是不同的。也就是说, j
始终指向 i
,不可改变。 j = k
只是把 k
的值给到了 j
,同时也改变了 i
。
若是还不懂,再来看一下指针的例子,你就明白了。
int i = 0;
// 定义指针 j ,指向 i
int *j = &i;
int k = 1;
// 指向另外一个对象
j = &k;
printf("%d, %d, %d\n", i, *j, k);
// 输出:0, 1, 1
// 打印地址
printf("%d, %d, %d\n", &i, j, &k);
// 输出:-1790365184, -1790365180, -1790365180
复制代码
看到了吗? j
在赋值了 &k
之后,地址就变成和 k
同样了,也就是说,指针 j
能够指向不一样的对象。这时候, j
和 i
就没有任何关系了,i
的值也不会随着 j
改变而改变。
引用最常出现的地方是做为函数的参数使用。
void change(int &i, int &j) {
int temp = i;
i = j;
j = temp;
}
int main() {
int i = 0;
int j = 1;
// 打印地址
printf("[before: %d, %d]\n", &i, &j);
//输出:[before: -224237816, -224237812]
change(i, j);
printf("[i: %d, j: %d]\n", i, j);
// 输出:i: 1, j: 0
// 打印地址
printf("[after: %d, %d]\n", &i, &j);
// 输出:after: -224237816, -224237812
return 0;
}
复制代码
在上面的例子中,change
方法的两个参数都是引用,和普通的参数有如下两个区别:
i. 引用参数不会建立新的内存块,参数只是对外部传进来的变量的一个引用。
ii. 引用参数能够改变外部变量的值。
这是普通变量的状况:
void change(int i, int j) {
int temp = i;
i = j;
j = temp;
// 打印地址
pritf("[change: %d, %d]\n", &i, &j);
// 输出[change: -1136723044, -1136723048]
}
int main() {
int i = 0;
int j = 1;
// 打印地址
printf("[before: %d, %d]\n", &i, &j);
//输出:[before: -224237816, -224237812]
change(i, j);
printf("[i: %d, j: %d]\n", i, j);
// 输出:i: 0, j: 1
// 打印地址
printf("[after: %d, %d]\n", &i, &j);
// 输出:after: -224237816, -224237812
return 0;
}
复制代码
能够看到,i
j
的值不会被改不变,缘由是 change
方法建立了两个临时的局部变量,都有本身的内存块,这个变量的地址和外部传进来的变量是没有关系的,因此没法改变外部变量的值。
到这里,就能够看到参数引用的好处了:引用参数为咱们节省了内存,执行效率也更快。
一样的,指针参数也有相似的效果,可是其仍然和引用有着本质的区别。引用为咱们提供另外一个种很好的传参选择。
有时候,咱们并不想让函数内部改变外部变量的值,能够给参数加上常量的标志。
void change(const int &i, const int &j) {
int temp = i;
i = j; // 不容许修改i,编译出错
j = temp; // 不容许修改j,编译出错
}
复制代码
多态
是面向对象的三大特色之一。
C++
的多态和 Java
很是类似,可是也有着明显的不一样。
看下面一个例子:
class A {
public:
void f() {
printf("a\n");
};
};
class B : public A {
public:
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 输出:a
return 0;
}
复制代码
这里 B
继承了 A
,并重写了方法 f
。
在 main
函数中,定义了一个基类变量指针 a
,并指向子类 B
。接着调用了 a
的方法 f
。
若是是 Java
中相似的操做的话,那么毫无疑问,此处会输出 b
,但是这里却输出了 a
。也就是说,这里方法 f
其实是基类 A
的 f
方法。
这就是 C++
和 Java
其中一个很大的不一样。
缘由是,调用函数 f() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态连接。
函数调用在程序执行前就准备好了。有时候这也被称为早绑定,由于 f() 函数在程序编译期间就已经设置好了。
那么若是想实现相似 Java
中的多态重载呢?
virtual
是 C++
中的一个关键字,用于声明函数,表示虚函数。用于告诉编译器不要静态连接到该函数,改成动态连接。
依然是上面的例子,在 A
的 f
函数上加上 virtual
,将获得相似 Java
的效果:
class A {
public:
virtual void f() {
printf("a\n");
};
};
class B : public A {
public:
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 输出:b
return 0;
}
复制代码
在 Java
中,咱们常常会使用 interface
或 abstract
来定义一些接口,方便代码规范和拓展,可是在 C++
没有这样的方法,可是能够有相似的实现,那就是:纯虚函数。
class A {
public:
// 声明一个纯虚函数
virtual void f() = 0;
}
class B : public A {
public:
// 子类必须实现 f ,不然编译不经过
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 输出:b
return 0;
}
复制代码
A
中的 virtual void f() = 0;
就是一个纯虚函数。若是继承 A
,子类必须实现 f
这个接口,不然编译不经过。
A
则是一个抽象类。不能被直接定义使用。
在 C++
中有一个方法,可让咱们在程序编译前,对代码作一些处理,称为预处理。这是 Java
中没有的,在 C++
中却常用到。
预处理是一些指令,可是这些指令并非 C++
语句,因此不须要以分号 ;
结束。
全部的预处理语句都是以井号 #
开始的。
好比 #include
就是一个预处理,用于将其余文件导入到一个另外一个文件中,相似 Java
的 import
。
例如导入头文件:
// A.h
class A{
public:
A();
~A();
}
复制代码
#include "A.h"
A::A() {
}
A::~A() {
}
复制代码
在 C++
中经常使用的预处理有如下几个 #include
、 #define
、#if
、#else
、 #ifdef
、 #endif
等。
最经常使用的一个预处理语句 #define
,一般称为宏定义。
其形式为:
#define name replacement-text
复制代码
#define PI 3.14159
printf("PI = %f", PI);
// 在编译以前,上面的语句被展开为:
// printf("PI = %f", 3.14159);
复制代码
#define SUM(a,b) (a + b)
printf("a + b = %d", SUM(1, 2));
// 在编译以前,上面的语句被展开为:
// printf("a + b = %d", 1 + 2);
// 输出:a + b = 3
复制代码
#
和 ##
运算符在宏定义中,#
用于将参数 字符串化
。
#define MKSTR( x ) #x
printf(MKSTR(Hello C++));
// 在编译以前,上面的语句被展开为:
// printf("Hello C++");
// 输出: Hello C++
复制代码
在宏定义中,##
用于将参数 链接起来
。
#define CONCAT(a, b) a ## b
int xy = 100;
printf("xy = %d", CONCAT(x, y));
// 在编译以前,上面的语句被展开为:
// printf("xy = %d", xy);
// 输出:xy = 100
复制代码
注意:#
、 ##
在多个宏定义嵌套使用的时候,会致使不展开的问题
例如:
#define CONCAT(x, y) x ## y
#define A a
#define B b
void mian() {
char *ab = "ab";
char *AB = "AB";
printf("AB = %s", CONCAT(A, B));
// 在编译以前,上面的语句被展开为:
// printf("AB = %s", AB);
}
复制代码
虽然定义了 A
B
两个宏定义,可是在 CONCAT
中遇到 ##
的时候,A
B
这两个宏定义是不会开展的,而是直接看成两个参数被链接起来了。
那么要如何解决这个问题呢?那就是再转接一层。
#define _CONCAT(x, y) x ## y
#define CONCAT(x, y) _CONCAT(x, y)
#define A a
#define B b
void mian() {
char *ab = "ab";
char *AB = "AB";
printf("AB = %s", CONCAT(A, B));
// 在编译以前,上面的语句被展开为:
// printf("AB = %s", _CONCAT(a, b));
// printf("AB = %s", ab);
// 输出:AB = ab
}
复制代码
#if
、#else
、 #ifdef
、 #endif
这几个的组合主要用条件编译。
在 C++
中条件编译也是常用到的,能够用来控制哪些代码参与编译,哪些不参与编译。
#define DEBUG
int main() {
#ifdef DEBUG
// 参与编译
printf("I am DEBUG\n");
#else
// 不参与编译
printf("No DEBUG\n");
#endif
return 0;
}
// 输出:I am DEBUG
复制代码
以上代码,因为先前已经定义了 #define DEBUG
因此 #ifdef DEBUG
为 true
,编译 printf("I am DEBUG\n");
。
若是去掉 #define DEBUG
,则编译 printf("No DEBUG\n");
。
int main() {
#if 0
// 这里面的代码都被注释掉,不参与编译
printf("I am not compiled\n");
#endif
return 0;
}
复制代码
以上,基本就是在 C++
常用到的,与 Java
类似,又存在差别的一些基础知识,因为面向对象语言都存在必定的类似性,相信有了以上的基础以后,你就能够比较通畅地阅读一些 C++
代码了。
若是你是一个 Java
程序员,可能对其中的一些知识仍是会感到迷惑,这时候须要你抛弃 Java
中的一些惯有思惟,从新细细品尝一下 C++
的味道,能够实际的去敲一下代码来消化这些知识,只有实践才能出真知。