本篇内容主要讲述C++使用时的一些注意事项及基础内容,主要有:
- 运算符,比较,循环,宏定义,常量,函数重裁等
- 引用和指针,sizeof, 内存泄漏的一些问题等
- C++面向对象特性:多态,继承相关等
内容偏向于笔记的记录,以及一些语法的坑点。
C++ 在现有的cocos2d-x引擎或cocosCreator引擎中使用较少,但理解引擎的设计需要有C++的一些基础,故此将本篇博客分享出来, 如果理解有误,欢迎指点一二!
对于习惯性使用Mac + Visual Studio Code的小伙伴,如果想配置C++的运行,推荐插件:
- CodeRunner
- C++
如果需要C++11的支持,可参考博客:visual Studio Code 支持C++11配置
基础
C/C++的头文件均以.h
为后缀。
C的定义文件以.c
为后缀,C++的定义文件以.cpp
为后缀(也有一些系统以.cc
或.cxx
为后缀)
运算符优先级从高到底排列顺序:
() [] -> .
! ~ ++ --
* / %
+ -
<< >>
< <= > >=
== !=
&
^
|
&& ||
?:
= += -= /= %= &= ^= |= <<== >>==
运算符较多的情况,可以使用()
来控制优先级,不要试图些一坨来彰显代码功底的深厚。
项目的开发,不是靠个人而是团队;所以增加注释和代码的简洁真的很好!
if 与零的比较
c++
// 与bool变量对比,不要使用TRUE,FALSE或者1,0,在不同的编译器中,TRUE或者FALSE的值没有标准
if(flag)
if(!flag)
// 整形变量可以使用 == 或者!= 与0比较
if(value == 0)
if(value != 0)
// 浮点型变量比较, 小数是很难比较的,因此采用精度相关
#define VALUE = 0.000001
if (value <= VALUE) && (value >= VALUE)
// 指针变量比较使用NULL, 将NULL放在前面,避免变为赋值
if(NULL == p)
if(p != NULL)
尤其注意的是浮点变量的比较,其他的更多的算是代码的一种安全规范!
循环
循环使用最多的是for,while, do_while
。
建议将循环最短的放在外层,这样做的原因是:减少CPU切循环层
c++
for(i = 0; i < 5; ++i) {
for(j = 0; j < 100; ++j) {}
}
循环变量主要不要随便修改,避免for循环失去控制, 尤其针对于vector
的使用。
常量
常量主要有两种形式:
- C语言中的
#define
- C++语言中的
const
两者相比:
#define
主要用于替换,没有数据类型的检测,如果使用不当容易出现问题,比如边际效应const
有数据类型,代码会进行类型检测;可修饰变量、函数参数、函数返回值和类成员等。
简单的示例:
c
// #define的使用 要增加() 避免出现问题
#define MAX_VALUE (100)
// const
const int MAX_NUM = 100;
在项目开发中关于define 也会这样使用:#ifndef/define/#endif
c
#ifndef __STUDIO_H__
#define __STUDIO_H__
#include <studio.h> // <>代表引用标准库文件,从标准库目录中查找
#include "public.h" // ""代表引用自定义的头文件相关,从工作目录中查找
#endif
这样设计的目的是为了防止头文件被重复引用。
const
用于修饰参数,表示仅做输入使用,不可修改
c
// 字符串的复制,将strSource复制到strDest中
char *StrCpy(char *strDest, const char *strSource) {
assert((strDest != NULL) && (strSource != NULL));
char *add = strDest;
while(*strDest++ = *strSource != '\0');
return add;
}
// 将源内存的内容复制到目标内存中
void *memcpy(void *pvTo, const void *pvFrom, size_t size) {
assert((pvTo != NULL) && (pvFrom != NULL));
// 使用临时变量防止改变地址, 且使用byte指针,以方便逐字节的复制操作
byte *pbTo = (byte *)pvTo;
byte *pbFrom = (byte *)pvFrom;
while(size-- > 0) {
// 先++后*
// pbTo和pvTo指向的都是同一个内存,pbTo的内容赋值也会修改pvTo指向区域的内容
*pbTo++ = *pbFrom++;
}
// 如上内容复制完成后,返回pvTo的起始地址即可,即可获取内容
return pvTo;
}
const
如果在类中使用,注意: 不可在声明中初始化,因为类对象还没有创建。
c
const int MAX_NUM = 100;
// 修饰类成员可以在类的构造方法中进行
// ERROR
class A {
const int SIZE = 100;
int array[SIZE]; // 类对象未创建,SIZE未知
}
// RIGHT
# .h
class A{
A(int size); // 构造函数
const int SIZE;
}
# .cpp
A::A(int size):SIZE(size){
}
const
有一个很有意思的特性:
c++
// 修饰的是指针的内容,也就是值不变
const int *p = NULL;
// 修饰的是地址,地址不变
int const *p = NULL;
// 修饰的地址和值,均不变
const int const *p = NULL;
在很久之前的端游项目开发中,还记得当年我的领导告诉我:
define
和const
的变量常用大写字母,使用下划线分割- 静态变量添加前缀
s_
表示static;全局变量添加g_
;类成员变量添加m_
引用
引用相当于一个人的外号,没有这个人也就没有外号。引用的特点是:
- 变量必须合法,不可以为
NULL
- 引用被创建的时候,必须被初始化,变量必须合法
- 引用的关系一旦确定,不可改变
经常拿指针和引用对比,相比较它的特点是:指针创建后,可以不初始化且为NULL
,可以随时改变对象。
简单的实例:
c
// 引用
int value = 10;
int &m = value;
cout << m << endl; // 10
// 指针
int *p = NULL;
p = &value;
cout << *p << endl; // 10
指针
使用指针主要有两种方式:
- C语言的
malloc/free
, 是库函数 - C++语言的
new/delete
,是运算符
new/delete
的底层实现依然是malloc/free
,但设为运算符的原因主要是为了保证类对象能够主动的调用构造和析构函数而设计。
两者相比较:
malloc/free
作为C的标准库函数;new/delete
作为C++运算符malloc
的创建需要通过sizeof
指定大小,且返回的void*
类型需要强制转换;new
不需要new/delete
的使用可以在类对象创建中主动调用构造函数,对象销毁时主动调用析构函数
简单的实例:
c
/*
malloc/free相关:
1. 要通过sizeof指定字节的大小
2. malloc创建成功后,返回的是 void* 指针,要进行强制类型转换
3. 要检测是否创建成功,销毁要置空,避免野指针
*/
int *p = (int *)malloc(sizeof(int) * len);
if (NULL != p) {
free(p);
p = NULL;
}
/*
new/delete相关:
1. 虽作为运算符,但内部实现是malloc/free
*/
int *p = new int(1); // 开辟单个空间
delete p;
int *p = new int[4]; // 开辟多个空间, 多个空间不可忽略[]
delete []p;
指针的内存分配方式,主要有:
- 静态分配, 在编译时进行的, 在程序运行中一直存在,直到结束。主要是static和global等变量使用
- 栈中分配,主要在函数运行的时候使用,函数执行结束后自动释放。
- 堆中分配,主要通过
malloc/new
手动创建,使用灵活,问题最多。
使用指针需要注意:
- 创建失败,却使用了它,可添加
if(NULL != p)
来判定 - 创建成功,没有初始化
- 忘记释放内存,造成内存泄漏
- 释放内存,忘记置空,出现野指针
- 针对于数组相关,操作越过边界;注意数组指向的地址和容量不可改变,但指针可以指向任意地方
- 编写方法调用了栈内存指针
简单的实例:
c
class Obj {
public:
Obj() {cout << "init" << endl;}
~Obj() {cout << "destory" << endl;}
void Init() {cout << "init" << endl;}
void Destory() {cout << "destory" << endl;}
};
// malloc/free 在构建动态内存后,必须调用Initialize()和Destory() 来完成初始化和清除工作
void UseMallFree(){
Obj *a = (Obj *)malloc(sizeof(Obj));
if (NULL != a) {
a->Init();
a->Destory();
free(a);
}
}
// new/delete 在构建动态内存后,会自动调用构造方法进行初始化,销毁时会自动调用析构进行销毁
void UseNewDelete() {
Obj *a = new Obj;
if (NULL != a) {
delete a;
}
}
内存思考的一些问题:
c++
// 方式1: 报错, GetMemory不可传递动态内存, str一直为NULL
void GetMemeory(char *p){
p = (char *)malloc(100);
}
void Test(){
char *str = NULL;
GetMemory(str);
strCpy(str, "Hello");
printf(str);
}
// 方式2: 内容未知,数据存在栈内存中,已经释放
void GetMemeory(){
char p[] = "hello";
return p;
}
void Test(){
char *str = NULL;
str = GetMemeory();
printf(str);
}
// 方式3: 能够正确输出,但未释放,导致内存泄漏了
void GetMemeory(char **p,int num){
*p = (char *)malloc(num);
}
void Test(){
char *str = NULL;
GetMemory(&str, 100);
strCpy(str, "Hello");
printf(str);
}
// 方式4:
void Test() {
char *str = (char*)mallo(100);// 问题一,创建后未检测是否创建成功
strCpy(str, "hello");
free(str); // 释放后, 未对str设置NULL,出现野指针
if (str != NULL){ // 再执行下,错误已经无法预料了
strcpy(str, "world");
printf(str);
}
}
sizeof
指针的malloc
的创建需要有sizeof
的支持,sizeof
主要用于计算内存容量的大小。
在不同的机器类型中,变量类型的字节数是不一致的。
c++
// 要考虑不同类型的机器,32位还是64位
// sizeof返回的是对象或类型的大小
cout << "char:" << sizeof(char) << endl; // char:1
cout << "char16_t:" << sizeof(char16_t) << endl; // char16_t:2
cout << "char32_t:" << sizeof(char32_t) << endl; // char32_t:4
cout << "unsigned int:" << sizeof(unsigned int) << endl; // unsigned int:4
cout << "int:" << sizeof(int) << endl; // int:4
cout << "float:" << sizeof(float) << endl; // float:4
cout << "double:" << sizeof(double) << endl; // double:8
cout << "long int:" << sizeof(long int) << endl; // long int:8
cout << "long long int:" << sizeof(long long int) << endl; // long long int:8
int n = 10;
cout << sizeof(n) << endl; // 4
int &m = n;
cout << sizeof(m) << endl; // 4
char a[] = "hello";
cout << sizeof(a) << endl; // 6 注意末尾有'\0'
// 如果为sizeof(p1/p2/p3/p4)为类型大小,一律为4
// 为sizeof(*p1)为int类型对象,为4
// 为sizeof(*p2),虽为数组,但指针大小不受对象个数影响,且求的单个元素的大小,故此是4
// 为sizeof(*p3) 对象大小数目待定,无法求取大小
// 为sizeof(*p4) 求的是指针对象的大小,为4
int *p1 = new int(1);
int *p2 = new int[100];
void *p3 = malloc(100);
int *p4 = (int *)malloc(sizeof(int));// 要考虑不同类型的机器,32位还是64位
cout << sizeof(char) << endl; // 1
cout << sizeof(int) << endl; // 4
int n = 10;
cout << sizeof(n) << endl; // 4
int &m = n;
cout << sizeof(m) << endl; // 4
char a[] = "hello";
cout << sizeof(a) << endl; // 6 注意末尾有'\0'
int *p1 = new int(1); // 指针相关一律4
int *p2 = new int[100];
void *p3 = malloc(100);
int *p4 = (int *)malloc(sizeof(int));
需要注意:
- 字符串的大小要考虑末尾的
\0
虽然它没有显示 - 指针对象的大小,不受个数影响
函数重载
函数重载主要被用于C++中, 它是类实现多态的一个特性,通常被称为静态多态。
函数重载有时候容易与类的重写virtual
相混淆,他们的区别是:
函数重载的特征:
- 相同的作用域,即在同一个类中
- 函数名字相同,参数个数或类型不同
virtual
的名字可有可无
覆盖具体指的是派生类覆盖基类的函数,特征有:
- 不同的作用域,分别位于派生类和基类中
- 函数名字相关,参数相关
- 必须在基类中生命,且带有
virtual
关键字。 - 可以实现多态(指的就是在不同的派生类中有着不同的实现)
- 可以作为纯虚函数来使用,即基类中声明,派生类中实现
在实际的应用中,函数重载需要注意返回值 ,使用不当容易出现类型二义性
c
bool setSkin (const char* skinName);
bool setSkin (const std::string& skinName);
函数重载的出现还有一个根本原因,就是: 类的构造函数需要与类重名
针对于函数重载,在程序运行中,编译器会根据函数的参数列表类型进行区分,生成一个由函数名和参数类型组成的字符串,用于区分不同的函数。比如:
c++
void foo(int a); // 生成 foo_int
void foo(int a, int b); // 生成 foo_int_int
C语言使用C++的函数要使用extern "C"
,主要原因就是编译器对函数编译后,C语言会生成*_foo*,而C++会生成*_foo_int*, 使用extern
可以进行连接交换。
c++
extern "C" {
#include "Header.h"
void foo(int x, int y);
}
inline
内联函数的使用,不仅要在声明中使用,也要在定义中使用。
c++
// 声明
inline void Foo(int x);
// 定义
inline void Foo(int x) {...}
内联函数的使用有助于提高函数的执行效率,但一般代码比较简单或者简短。
主要原因在于: 它以复制代码为代价,虽降低了代码调用的开销,但会让总代码量更大,消耗更多的空间。
面向对象
类是C++中的核心特性,通过对象的方式来被访问。包含两个特殊函数:
- 构造函数, 与类名同名, 主要用于构建对象
- 析构函数,增加了
~
,与类名同名,主要用于销毁对象
类的特点主要有:
- 封装 保护类中的数据,且向外面暴露的接口,可将细节隐藏起来
- 继承 实现代码功能的复用,派生的目的可以实现拓展
- 多态 增加接口重用和代码的可扩充性
基类的成员访问权限 | 继承方式 | 派生类的成员访问权限 |
---|---|---|
public | public | public |
protected | public | protected |
private | public | ~ |
public | protected | protected |
protected | protected | protected |
private | protected | ~ |
public | private | private |
protected | private | private |
private | private | ~ |
简言之就是:
- 被派生类pulic继承的对象,基类的public,protected对象会被派生类作为public, protected使用
- 被派生类protected继承的对象, 基类的public, protected对象会被派生类作为protected使用
- 被派生类private继承的对象,基类的public, protected对象会被派生类作为private使用
继承和多态
如果继承,对象构建时,先调用基类的构造函数,再调用子类的。
对象销毁时,先调用子类的析构函数,再调用基类的析构函数。
c
class Base {
public:
Base() {cout << "Base" << endl;}
~Base() {cout << " ~Base" << endl;}
void testA() {cout << "testA" << endl;}
};
class DeriveA: public Base {
public:
DeriveA() {cout << "DeriveA" << endl;}
~DeriveA() {cout << " ~DeriveA" << endl;}
void testB() {cout << "testB" << endl;}
};
int main() {
// 基础对象
DeriveA A;
A.testA();
A.testB();
return 0;
}
/*
Base -- 先调用基类的构造函数
DeriveA -- 再调用子类的构造函数
testA
testB
~DeriveA -- 释放时,先调用子类的析构函数
~Base -- 释放时,再调用基类的析构函数
*/
在继承当中,经常会考虑virtual重写的情况。
c
class Base {
public:
void testA() {cout << "testA" << endl;}
// 虚函数,子类继承后,会覆盖
virtual void testB() {cout << "Base testB" << endl;}
// 纯虚函数,在子类中实现, 多用于抽象类
virtual void testC() = 0;
};
class Derive: public Base {
public:
void testA() {cout << "testB" << endl;}
virtual void testB() {cout << "Derive testB" << endl;}
virtual void testC() {cout << "Derive testC" << endl;}
};
int main()
{
Derive B;
Base *p = &B;
p->testA(); // testA // 静态继承,调用基类
p->testB(); // Derive testB // 动态继承,调用自己的
p->testC(); // Derive testC // 纯虚实现
return 0;
}
重写,主要通过virtual
虚函数来实现,与函数重载相比较,它属于动态多态。
它主要有两种使用方式:
- 虚函数的使用,一般用于重写
- 纯虚函数的使用,一般用于抽象实现,基类仅声明,子类负责实现。
c
// 纯虚函数,不仅需要有virutal,还需要在函数结尾处增加 = 0
virtual bool checkAvailable() = 0;
注意:
- 类的构造函数不可以为虚函数。虚函数的使用需要通过虚函数表,而虚函数表必须在创建对象后才有,也就是构造函数执行结束后才能初始化
- 析构函数可以虚函数,且尽量为虚函数,主要为了避免:指针对象,且基类的指针指向子类,在对象释放时,只会调用基类的析构,却不会调用子类的析构,从而导致内存泄漏
c
class A {
public:
// 将基类析构virtual
virtual ~A() {cout << "A::~A" << endl;}
};
class B : public A {
public:
~B() {cout << "B::~B" << endl;}
};
int main() {
/*
构建基类指针,指向子对象的
如果基类的析构函数没有virtual,结果就是:
A::~A
没有释放子类的析构相关,会导致子类对象内存泄漏
如果基类的析构函数virtual, 结果就是
B::~B
A::~A
此种情况是正确的
*/
A *p = new B();
delete p;
/*
如下的情况没有问题,指向子类相关,释放的时候先释放子类的,再释放基类的
B::~B
A::~A
*/
B *p2 = new B();
delete p2;
return 0;
}
如果继承, 注意继承二义性,比如: D继承B和C, B和C继续A,他们都存在一个共同的方法,实现不同的逻辑。
其他
某些时候会将构造函数和析构函数设为private
,主要是为了保证对象的唯一实例,多用于单例模式
c
class PoolManager {
public:
static PoolManager* getInstance(); // 确保动态对象的单一性
static void destroyInstance();
private:
PoolManager(); // 构造函数私有化, 保证创建对象的单一性,通过getInstance来构建
~PoolManager(); // 析构函数私有化,保证只能在堆中创建对象
}
PoolManager* PoolManager::getInstance() {
if (s_singleInstance == nullptr) {
s_singleInstance = new (std::nothrow) PoolManager();
}
return s_singleInstance;
}
// 析构函数私有化,通过提供成员方法进行释放
void PoolManager::destroyInstance() {
delete s_singleInstance;
s_singleInstance = nullptr;
}
String
c
// .h
class String {
public:
String(const char *str = NULL;) // 普通构造函数
String(const String &other); // 拷贝构造函数
String &operate = (const String &other); // 赋值函数
~String();
private:
char *m_data;
};
// .cpp
String::~String() {
delete []m_data;
}
String::String(const char *str) {
if (NULL == str) {
m_data = new char[1];
*m_data = '\0';
}
else {
int len = strlen(str) + 1; // 不可忽略'\0'的末尾字符
m_data = new char[len];
if (NULL != m_data) {
strcpy(m_data, str);
}
}
}
String::String(const String &other) {
int len = strlen(other.m_data) + 1;
m_data = new char[len];
if (NULL != m_data) {
strcpy(m_data, other.m_data);
}
}
String &String::operate=(const String &other) {
if (this == &other) {
return this;
}
delete []m_data;
int len = strlen(other.m_data) + 1;
m_data = new char[len];
if (NULL != m_data) {
strcpy(m_data, other.m_data);
}
return *this;
}