C++零散知识点
C++零散知识点
1 常量的定义原则
1.1 原则
对于单纯的常量,以枚举或者const对象取代#define。
1.2 代码示例
#include <iostream>
using namespace std;
class Person
{
public:
enum Occupation_Type // 采用枚举值定义常量
{
STUDENT = 0,
TEACHER = 1,
};
const static int MAX_NUM = 100; // 采用const static 定义常量
};
int main()
{
cout << Person::STUDENT << endl;
cout << Person::MAX_NUM << endl;
return 0;
}
2 explict关键字
2.1 default 构造函数
是一个可被调用而不带任何实参者,这样的构造函数要不没有参数,要不就是每个参数都有缺省值。
2.2 explicit关键字
此关键字可以阻止它们被用来执行隐式类型转换,但是仍然可以被用来进行显式类型转换。
2.3 explicit 使用案例
// Demo.h文件
#ifndef DEMO_H
#define DEMO_H
class Demo
{
public:
explicit Demo(int nValue = 10);
~Demo();
public:
void ShowValue();
private:
int m_nValue;
};
void DoSomething(Demo demo);
#endif // DEMO_H
// Demo.cpp文件
#include "Demo.h"
#include <QDebug>
Demo::Demo(int nValue) : m_nValue(nValue)
{
}
Demo::~Demo()
{
}
void Demo::ShowValue()
{
qDebug() << "Value : " << m_nValue << "\n";
}
void DoSomething(Demo demo)
{
demo.ShowValue();
}
// 使用处
#include <QApplication> // 应用程序类
#include "Demo.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv); // 应用程序对象,Qt中有且只有一个应用程序对象
// 此若不加explicit,处会进行隐式转换
DoSomething(10);
// 若加了 explicit,则无法进行隐式转换
DoSomething(10); // 报错
DoSomething(static_cast<Demo>(10)); // 可以进行显式转换
return a.exec(); // a.exec()进入消息循环机制,阻塞代码
}
3 延后变量定义式
3.1 示范程序
const int MIN_PASSWORD_LENGTH = 6;
string EncryptPassword(const string& strPassword)
{
string strEncrypted;
if (strPassword.length() < MIN_PASSWORD_LENGTH)
throw logic_error("Password is too short");
// ... 进行加密操作
return strEncrypted;
}
3.2 程序分析
如果抛出异常,仍然需要付出strEncrypted的构造与析构成本,所以最好延后strEncrypted的定义,直到真正需要它。
string EncryptPassword(const string& strPassword)
{
if (strPassword.length() < MIN_PASSWORD_LENGTH)
throw logic_error("Password is too short");
string strEncrypted;
strEncrypted = strPassword;
// ... 进行加密操作
// Encrypt(strEncrypted);
return strEncrypted;
}
3.3 程序优化
string EncryptPassword(const string& strPassword)
{
if (strPassword.length() < MIN_PASSWORD_LENGTH)
throw logic_error("Password is too short");
string strEncrypted(strPassword);
// ... 进行加密操作
// Encrypt(strEncrypted);
return strEncrypted;
}
备注:“通过default构造函数构造出一个对象然后对它进行赋值”比“直接在构造时指定初始值”效率差。
你不只应该延后变量的定义直到非得使用该变量前一刻为止,甚至应该尝试延后这份定义直到能够给它初始实参为止。
- 避免构造(和析构)非必要对象
- 避免无意义的default构造行为
3.4 变量在循环中使用的情况
变量在循环内使用,定义在循环外还是循环内。
方法A:定义于循环外
Widget w;
for (int i = 0; i < n; ++i)
{
w = 取决于i的某个值;
// ...
}
方法B:定义于循环内
for (int i = 0; i < n; ++i)
{
Widget w(取决于i的某个值);
// ...
}
方法A的成本:1个构造函数,1个析构函数,n个赋值运算符
方法B的成本:n个构造函数,n个析构函数
一般情况下选择B,A造成变量的作用域(覆盖了整个循环)比B更大,那么对程序的可理解性和易维护性造成冲突。
除非下列情况下选择A:
- 你直到赋值的成本比“构造+析构”成本低
- 你正在处理的代码重效率高度敏感的部分
4 尽量少做转型动作
4.1 C++的转型方式
旧式转型
- (T)expression // 将expression转换为类型T
- T(expression) // 将expression转换为类型T
新式转型
- const_cast(expression):将对象的常量性转除
- static_cast(expression):用来强迫隐式转换
- dynamic_cast(expression):用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
- reinterpret_cast(expression):用来执行低级转型,实际动作(及结果)可能取决于编译器,也意味着不可移植。例如:将pointer to int 转型为 int。
int nValue = 10;
const int* pA = &nValue;
// int* pB = pA; // 常规赋值会报错
int* pB = const_cast<int*>(pA);
4.2 转型示范代码
在调用一个explicit构造函数将一个对象传递给一个函数参数时。
class Widget
{
public:
explicit Widget(int nSize) : m_nSize(nSize)
{
}
void ShowWidget() const
{
cout << "Size = " << m_nSize << "\n";
}
private:
int m_nSize;
};
void DoSomeWork(const Widget& widget)
{
widget.ShowWidget();
}
void Test()
{
DoSomeWork(Widget(10)); // 旧式转型
DoSomeWork(static_cast<Widget>(20)); // 新式转型
}
4.3 转型时候的注意点
class Window
{
public:
virtual void OnResize()
{
}
};
class SpecialWindow : public Window
{
public:
virtual void OnResize() override
{
static_cast<Window>(*this).OnResize();
// 进行SpecialWindow的专属行为
}
};
备注:它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象只base class成分”的暂时副本身上的OnSize()
正确调用方法:
class SpecialWindow : public Window
{
public:
virtual void OnResize() override
{
Window::OnResize(); // 正确的调用方法
// 进行SpecialWindow的专属行为
}
};
验证方法:
class Window
{
public:
virtual void OnResize()
{
cout << "Address:" << &m_nValue << endl;
}
protected:
int m_nValue;
};
class SpecialWindow : public Window
{
public:
virtual void OnResize() override
{
cout << "Address:" << &m_nValue << endl;
Window::OnResize(); // 输出的地址相同
// static_cast<Window>(*this).OnResize(); // 输出的地址不同
// 进行SpecialWindow的专属行为
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Window* pWindow = new SpecialWindow();
pWindow->OnResize();
return a.exec();
}
尽量不要使用如下方式进行转型:
#include <QCoreApplication>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Window
{
public:
Window()
{
}
virtual ~Window()
{
}
protected:
int m_nValue;
};
class SpecialWindow : public Window
{
public:
void Blink()
{
}
};
typedef vector<shared_ptr<Window>> VecSPWin;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
VecSPWin vecPtrs;
VecSPWin::iterator iter = vecPtrs.begin();
while(iter != vecPtrs.end())
{
if (SpecialWindow* pSpeWindow = dynamic_cast<SpecialWindow*>(iter->get()))
pSpeWindow->Blink();
++iter;
}
return a.exec();
}
备注:在使用dynamic_cast时基类必须是虚析构函数
应该使用这样的方式进行:
typedef vector<shared_ptr<SpecialWindow>> VecSPSpeWin;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
VecSPSpeWin vecPtrs;
VecSPSpeWin::iterator iter = vecPtrs.begin();
while(iter != vecPtrs.end())
{
(*iter)->Blink();
++iter;
}
return a.exec();
}
最优方法:(消除了dynamic_cast)
#include <QCoreApplication>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Window
{
public:
Window()
{
}
virtual ~Window()
{
}
virtual void Blink()
{
}
protected:
int m_nValue;
};
class SpecialWindow : public Window
{
public:
virtual void Blink() override
{
}
};
typedef vector<shared_ptr<Window>> VecSPWin;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
VecSPWin vecPtrs;
VecSPWin::iterator iter = vecPtrs.begin();
while (iter != vecPtrs.end())
{
(*iter)->Blink();
++iter;
}
return a.exec();
}
4.4 总结:
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的代替设计。
- 如果转型是必要的,我们应该隔离转型动作,试着将它隐藏于某个函数背后。客户可以调用该函数,而不需要将转型放进他们自己的代码中。
- 宁可使用C++style(新式)转型,不要使用旧式转型,前者容易辨识出来,而且也比较有着分门别类的职掌。
5 函数异常安全
5.1 函数异常安全的条件
5.1.1 不泄漏任何资源
5.1.2 不允许数据败坏
5.2 函数异常安全的三个层次的保证
5.2.1 有效状态保证
如果异常被抛出,程序内的任何事物仍然保持在有效状态下,也就是说没有任何对象或数据结构会因此而破坏。程序可能处于任何状态,状态不可预料,只知道状态合法。
5.2.2 状态不变保证
如果异常被抛出,程序状态不改变,也就是说,如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
5.2.3 不抛掷保证
承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(int,指针等等)身上的所有操作都提供 nothrow 保证。
5.3 throw关键字
5.3.1 throwd的概念
它是函数提供者和使用者的一种君子协定,标明该函数不抛出任何异常,之所以说是君子协定,是因为实际上内部实现是需要人肉确保。
5.3.2 throw书写方式的含义
- Func() throw(type) 会抛出某种异常
- Func() throw() 不会抛出
- Func() throw(…) 可能是任何类型的异常
5.3.3 备注
如果一个标明throw()的函数内部发生了throw:
1.如果内部直接throw something,编译器会发现并指出;
2.如果是内部调用了一个可能throw something的函数,编译器无法发现,运行时一旦这个内部的函数throw,程序会abort。
5.4 异常安全函数程序案例
5.4.1 程序代码
class PrettyMenu
{
public:
void ChangeBackGround(istream& imgSrc);
private:
mutex m_Mutex;
Image* m_pImage;
int m_nImageChanges;
};
void PrettyMenu::ChangeBackGround(istream& imgSrc)
{
lock(&m_Mutex);
delete m_pImage;
m_nImageChanges++;
m_pImage = new Image(imgSrc);
unlock(&m_Mutex);
}
5.4.2 问题分析
上述函数在 new Image(imgSrc) 导致异常时,unlock将不会执行,于是互斥器将一直被锁住,造成资源泄漏。
上述函数在 new Image(imgSrc) 导致异常时,m_nImageChanges的数据已经被累加,而其实并没有新的图像被成功安装,造成数据败坏。
5.4.3 问题解决
5.4.3.1 解决资源泄漏问题
void PrettyMenu::ChangeBackGround(istream& imgSrc)
{
Lock lo(&m_Mutex);
delete m_pImage;
m_nImageChanges++;
m_pImage = new Image(imgSrc);
}
采用Lock的方式,获得互斥器并确保它稍后一定能释放。
我们还可以采用一些其他的方式,使得ChangeBackGround一旦有异常抛出时,PrettyMenu对象可以继续拥有原背景图像,或者令它拥有某个缺省的背景图像。
5.4.3.2 解决数据败坏问题
class PrettyMenu
{
public:
void ChangeBackGround(istream& imgSrc);
private:
mutex m_Mutex;
tr1::shared_ptr<Image> m_spImage; // 采用智能指针的解决方式
int m_nImageChanges;
};
void PrettyMenu::ChangeBackGround(istream& imgSrc)
{
Lock lo(&m_Mutex);
m_spImage.reset(new Image(imgSrc)); // 采用智能指针之后不用手动delete
++m_nImageChanges;
}
以上操作可以达到有效状态保证的效果
5.4.3.3 追求状态不变保证
采用copy and swap策略,为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改,若任何修改动作抛出异常,原对象仍然保持未改变状态。所有的改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
处理方式时将“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法被称为pimpl idiom。
struct Impl
{
tr1::shared_ptr<Image> m_spImage;
int m_nImageChanges;
};
class PrettyMenu
{
public:
void ChangeBackGround(istream& imgSrc);
private:
mutex m_Mutex;
tr1::shared_ptr<Impl> m_spImpl;
};
void PrettyMenu::ChangeBackGround(istream& imgSrc)
{
using std::swap;
Lock lo(&m_Mutex);
// 创建副本
shared_ptr<Impl> spNew(new Impl(*m_spImpl));
// 修改副本
spNew->m_spImage.reset(new Image(imgSrc));
++spNew->m_nImageChanges;
// 置换数据
swap(m_spImpl, spNew);
}
补充:
“copy-and-swap”策略并不能保证整个函数有强烈的异常安全性,如下所示
void DoSomething()
{
// 对local状态做一份副本
F1();
F2();
// 将修改后的状态置换回来
}
若F1或F2的异常安全性比“状态不变保证型”低,就很难让DoSomething成为“状态不变保证型”。
5.5 总结
- 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构破坏,这样的函数区分为三种可能的保证:有效状态保证型,状态不变保证型,不抛异常型。
- 状态不变保证型往往能以copy-and-swap实现出来,但状态不变保证型并非对所有函数都可实现或者具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中最弱者。
6 多态使用的好处
6.1 对比示范
// 不使用多态的方式
class Shape
{
public:
enum Shape_Type
{
CIRCLE = 0,
RECTANGLE = 1,
TRIANGLE = 2
};
public:
Shape_Type m_eType;
};
void DrawShapes(list<Shape*> listShape)
{
list<Shape*>::iterator pIter = listShape.begin();
list<Shape*>::iterator pEnd = listShape.end();
for (; pIter != pEnd; ++pIter)
{
switch ((*pIter)->m_eType)
{
case Shape::CIRCLE:
// 绘制圆形
break;
case Shape::RECTANGLE:
// 绘制矩形
break;
case Shape::TRIANGLE:
// 绘制三角形
break;
default:
}
}
}
// 使用多态的方式
class Shape
{
public:
virtual void Draw() = 0;
};
/*****************************/
class Circle :public Shape
{
public:
void Draw() override;
};
class Rectangle :public Shape
{
public:
void Draw() override;
};
class Triangle :public Shape
{
public:
void Draw() override;
};
void Circle::Draw()
{
cout << "绘制圆形\n";
}
void Rectangle::Draw()
{
cout << "绘制矩形\n";
}
void Triangle::Draw()
{
cout << "绘制三角形\n";
}
void DrawShapes(list<Shape*> listShape)
{
list<Shape*>::iterator pIter = listShape.begin();
list<Shape*>::iterator pEnd = listShape.end();
for (; pIter != pEnd; ++pIter)
(*pIter)->Draw();
}
7 多重继承菱形问题
7.1 菱形问题的内存解释
class A
{
// A的内存为A
};
class B :public A
{
// B的内存为A+B
};
class C :public A
{
// C的内存为A+C
};
class D :public B, public C
{
// C的内存为A+B+A+C+D
};
8 C文件I/O
8.1 文件类型
> 文本文件
又称为ASCII文件,文件中每个字节存放一个ASCII码,代表一个字符。
> 二进制文件
把内存中数据按照其在内存中的存储形式原样输出到磁盘上存放。
8.2 文件的打开与关闭
8.2.1文件打开
FILE* pFile = fopen(filePath, mode);
第一个参数:文件路径
第二个参数:打开方式
常用打开方式列表:
| 打开方式 | 含义 |
|---|---|
| “r” | 只读,文件必须存在 |
| “w” | 只写,文件不存在时创建文件,文件存在时原数据清零 |
| “a” | 追加,向文件末尾增加数据 |
| “rb” | 二进制,只读 |
| “wb” | 二进制,只写 |
| “ab” | 二进制,追加 |
| “r+” | 读写,打开一个文本文件 |
| “w+” | 读写,创建一个文本文件 |
8.2.2文件关闭
fclose(pFile); // pFile为要关闭的文件指针
8.3 文件的读写
8.3.1 fputc和fgetc
char result = fputc(ch, pFile); // 写入一个字符
char result = fgetc(pFile); // 读取一个字符
8.3.2 fwrite和fread
size_t fwrite(const void* pBuffer, size_t nSize, size_t nCount, FILE* pFile);
参数
- pBuffer:一个指针,被写入数据的地址
- nSize:写入的字节数
- nCount:进行写入nSize字节的数据项的个数
- pFile:文件指针
返回值
如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。
size_t fread(void* pBuffer, size_t nSize, size_t nCount, FILE* pFile);
参数
- pBuffer:一个指针,读取数据的地址
- nSize:读取的字节数
- nCount:进行读取nSize字节的数据项的个数
- pFile:文件指针
返回值
成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
8.3.3 注意点
读写最好采用配套操作,写如果用了"b"标记,读也最好用"b"标记。
8.4 程序案例
8.4.1 fputc和fgetc的读写
#pragma warning(disable : 4996)
#include <iostream>
using namespace std;
class FileIO
{
public:
FileIO(const string& strPath, const string& strMode);
~FileIO();
public:
bool Initialize();
bool WriteToFile(const string& strData);
bool ReadFromFile(string& strData);
void Release();
private:
string m_strPath;
string m_strMode;
FILE* m_pFile;
bool m_bInitOk;
};
FileIO::FileIO(const string& strPath, const string& strMode) :
m_strPath(strPath),
m_strMode(strMode),
m_pFile(nullptr),
m_bInitOk(false)
{
// ...
}
FileIO::~FileIO()
{
Release();
}
bool FileIO::Initialize()
{
m_pFile = fopen(m_strPath.c_str(), m_strMode.c_str());
if (nullptr == m_pFile)
{
m_bInitOk = false;
cout << "Create Or Open File Failed\n";
return false;
}
m_bInitOk = true;
return true;
}
bool FileIO::WriteToFile(const string& strData)
{
if (!m_bInitOk)
{
cout << "m_bInit Is False\n";
return false;
}
size_t nLength = strData.length();
for (size_t i = 0; i != nLength; ++i)
{
// 如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。
if (EOF == fputc(strData.at(i), m_pFile))
{
cout << "Write File Failed\n";
return false;
}
}
return true;
}
bool FileIO::ReadFromFile(string& strData)
{
if (!m_bInitOk)
{
cout << "m_bInit Is False\n";
return false;
}
strData.clear();
// feof(m_pFile):此函数用于判断文件是否结束
while (!feof(m_pFile))
{
char result = fgetc(m_pFile);
if (EOF != result)
strData.append(string(1, result));
}
return true;
}
void FileIO::Release()
{
if (nullptr != m_pFile)
{
fclose(m_pFile);
m_pFile = nullptr;
}
}
备注:在读取文件字符时不要使用下面的方式判断是否到文件尾部,因为文件中假设存在一个值为-1的字符,那么将会误判文件已经到尾部。
char result;
while(EOF != result)
{
result = fgetc(m_pFile);
}
8.4.2 fgets函数读取文件
char* fgets(char* str, int nLength, FILE* pFile);
参数
- str:这是指向一个字符数组的指针,该数组存储了要读取的字符串。
- nLength:这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
- pFile:这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
返回值
如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。
备注
fgets函数读取一行,在遇到\r\n时,\r被舍弃,只保留\n
| 字符简称 | 含义 | 字符 | ASCII值 |
|---|---|---|---|
| CR | Carriage Return 回车 | ‘\r’ | ASCII为13 |
| LF | LineFeed 换行 | ‘\n’ | ASCII码为10 |
Unix/Linux换行使用:LF(0x0A)
Dos/Windows使用的是:CRLF(0x0D0A)
void Test()
{
FILE* pFile = fopen("Config.txt", "r");
if (nullptr == pFile)
{
cout << "Open File Failed\n";
return;
}
char szBuffer[1024];
while (!feof(pFile))
{
memset(szBuffer, '\0', sizeof(szBuffer));
char* pResult = fgets(szBuffer, sizeof(szBuffer) - 1, pFile); // 减1为了保证最后一个字符为'\0'
if (nullptr == pResult)
continue;
// 清除行尾的回车符合换行符 回车:0x0D 换行:0x0A
int nLength = strlen(szBuffer);
if (nLength <= 0)
continue;
while ('\r' == szBuffer[nLength - 1] || '\n' == szBuffer[nLength - 1])
{
szBuffer[nLength - 1] = '\0';
nLength = strlen(szBuffer);
if (nLength <= 0)
break;
}
printf("Line Data : %s\n", szBuffer);
}
fclose(pFile);
}
8.4.3 fwrite和fread的读写
采用二进制方式进行结构体数据的读写
void WriteStudentData()
{
Student arrStu[2];
strcpy(arrStu[0].m_szName, "zhangsanA");
arrStu[0].m_nAge = 21;
arrStu[0].m_dScore = 99.5;
strcpy(arrStu[1].m_szName, "zhangsanB");
arrStu[1].m_nAge = 22;
arrStu[1].m_dScore = 92.5;
FILE* pFile = fopen("struct_file.bin", "wb");
if (nullptr == pFile)
{
cout << "Create File Failed\n";
return;
}
// 一般可以不要 nResult 结果
size_t nResult = fwrite(arrStu, sizeof(Student), 2, pFile);
fclose(pFile);
}
void ReadStudentData()
{
Student arrStu[2];
FILE* pFile = fopen("struct_file.bin", "rb");
if (nullptr == pFile)
{
cout << "Open File Failed\n";
return;
}
size_t nResult = fread(arrStu, sizeof(Student), 2, pFile);
fclose(pFile);
}
新特性(>=C++11)
1 auto关键字
1.1 auto关键字的新意义
- auto表示一个类型指示符,用来提示编译器对此类型的变量做类型的自动推导。
- auto并不能代表一个实际的类型声明,只是一个类型声明的“占位符”。
- 使用auto声明变量必须马上初始化,以让编译器推断出它的实际类型,并在编译时将auto占位符替换为真正的类型。
auto的一些基本用法
auto nValue = 5; // 推导nValue为int类型
auto pValueA = new auto(1); // 推导pValueA为int*类型
const auto* pValueB = &nValue; // 推导pValueB为const int* 类型
static auto dValue = 0.0; // 推导dValue为static double类型
1.2 auto的推导规则
int x = 5;
auto* a = &x; // a->int* auto->int
auto b = &x; // b->int* auto->int*
auto& c = x; // c->int& auto->int
auto d = c; // d->int auto->int d不会被推导成引用类型
const auto e = x; // e->const int auto->int
auto f = e; // f->int auto->int f不会推导为const
const auto& g = x; // g->const int& auto->int
auto h = g; // h->const int& auto->const int&
const auto* j = &x; // j->const int* auto->int
auto k = j; // k->const int* auto->const int*
- 当不声明为指针或引用时,auto的推导结果和初始化表达式抛弃引用和cv限定符后的类型一致。
- 当声明为指针或者引用时,auto的推导结果将保持初始化表达式的cv属性。
备注:cv限定符是const和volatile限定符的统称。
1.3 auto的推导限制
- auto不能用于函数参数
- auto不能用于非静态成员变量
- auto无法定义数组
- auto无法推导出模板参数
void Fun(auto nVal = 0) // error:auto无法用于函数参数
{
}
struct Foo
{
auto var1 = 1; // error:auto无法用于非静态成员变量
static const auto var2 = 10;
};
template <typename T>
struct Bar
{
};
int main(int argc, const char * argv[]) {
int arrA[10] = { 0 };
auto aa = arrA; // OK:aa为int*类型
auto arrB[10] = arrA; // error:auto无法定义数组
Bar<int> barA;
Bar<auto> barB = barA; // error:auto无法推导模板参数
return 0;
}
1.4 常见使用auto推导的场景
- 在声明一些stl容器的迭代器时候,往往声明冗长,auto可以简化声明代码
- 有些情况下我们无法知道变量类型该被定义成什么类型的时候
#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;
int main(int argc, const char * argv[])
{
map<int, string> resultMap;
// ...
auto iter = resultMap.begin();
for(; iter != resultMap.end(); ++iter)
{
// do something
}
return 0;
}
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map> // 需要的头文件
using namespace std;
int main(int argc, const char * argv[])
{
unordered_multimap<int, char> resultUnorderMap;
int nIndex = 0;
for (int i = 0; i != 10; ++i)
{
resultUnorderMap.insert(pair<int, char>(i, 'A' + (nIndex++)));
resultUnorderMap.insert(pair<int, char>(i, 'A' + (nIndex++)));
}
/*
pair<unordered_multimap<int, char>::iterator, unordered_multimap<int, char>::iterator> range = resultUnorderMap.equal_range(1);
*/
// 上面冗长的部分可以用auto代替
auto range = resultUnorderMap.equal_range(1);
return 0;
}
class Foo
{
public:
static int Get()
{
return 0;
}
};
class Bar
{
public:
static const char* Get()
{
return "0";
}
};
template <typename T>
void Fun()
{
auto value = T::Get();
cout << value << endl;
}
int main(int argc, const char * argv[])
{
Fun<Bar>();
return 0;
}
2 列表初始化
2.1 一般列表初始化
五种列表初始化
class Foo
{
public:
Foo(int nValue, double dValue)
{
}
};
// 5.列表初始化使用在函数返回值上
Foo Func()
{
return { 123, 1.1 };
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Foo fooA{ 123, 1.1 }; // 1.不带等号的列表初始化
Foo fooB = { 123, 1.1 }; // 2.带等号的列表初始化
int* pA = new int { 123 }; // 3.指向单个数据的指针列表初始化
int* pArr = new int[3] { 1, 2, 3 }; // 4.指向数组数据的指针列表初始化
return a.exec();
}
列表初始化可以被用于自定义类型的初始化,对于一个自定义类型,列表初始化表现在可能有两种执行结果。
struct Point
{
int m_nX;
int m_nY;
};
struct DefPoint
{
int m_nX;
int m_nY;
DefPoint(int, int) : m_nX(0), m_nY(0)
{
// ...
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Point pA{ 1, 2 };
DefPoint pB{ 1, 2 };
return a.exec();
}
- pA的初始化过程聚合类型的初始化,它将以拷贝的形式,用初始化列表中的值来初始化Point中的成员。
- pB定义了一个自定义的构造函数,因此实际上是以构造函数进行的。
此处引出一个概念:聚合类型的定义
- 类型是一个普通的数组(如int[10],char[],long[2][3])
- 类型是一个类(class,struct,union):且满足以下条件
- 无用户自定的构造函数
- 无私有(private)或保护(protected)的非静态数据成员
- 无基类
- 无虚函数
- 不能有{}和=直接初始化的非静态数据成员
对于数组而言,只要该类型是个普通数组,哪怕数组元素并非一个聚合类型,这个数组本身也是一个聚合类型。
struct Point
{
int m_nX = 0; // 直接初始化的非静态数据成员
int m_nY;
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Point pA{ 1, 2 }; // error:不能有{}和=直接初始化的非静态数据成员
return a.exec();
}
对于非聚合类型想要使用初始化列表的方法就是自定义一个构造函数。
备注:聚合类型的定义并非递归的,当一个类的非静态成员变量是非聚合类型时,这个类也有可能是聚合类型。
struct ST // ST为非聚合类型
{
int m_nX;
double m_dY;
private:
int m_nZ;
};
struct Foo // Foo为聚合类型
{
ST st;
int nX;
double dY;
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Foo foo{ {}, 1, 1.1 }; // {}相当于调用ST的无参构造函数
return a.exec();
}
总结:
- 聚合类型:使用初始化列表相当于对其中的每个元素分别赋值。
- 非聚合类型:需要自定义一个合适的构造函数,此时使用初始化列表将调用它对应的构造函数。
2.2 initializer_list列表初始化
int arr[] = { 1, 2, 3 };
map<string, int> mapData =
{
{ "1", 1 },
{ "2", 2 },
{ "3", 3 }
};
set<int> setData = { 1, 2, 3 };
vector<int> vecData = { 1, 2, 3 };
stl中的容器是通过使用initializer_list这个轻量级的类模板来完成上述功能支持的。我们也可以使用这种方式对自定义类型添加一个为initializer_list类型的参数,来实现这种任意长度的初始化能力。
class Foo
{
public:
Foo(initializer_list<int> listData)
{
for (auto iter = listData.begin(); iter != listData.end(); ++iter)
cout << *iter << endl;
}
};
class FooMap
{
public:
using pair_t = map<int, int>::value_type; // pair_t类型为pair<int, int>
public:
FooMap(initializer_list<pair_t> listData)
{
for (auto iter = listData.begin(); iter != listData.end(); ++iter)
m_ContentMap.insert(*iter);
}
private:
map<int, int> m_ContentMap;
};
Foo foo = { 1, 2, 3, 4, 5 };
initializer_list不仅可以用来对自定义类型做初始化,还可以用来传递同类型的数据集合。
void Func(initializer_list<int> listData)
{
for (auto iter = listData.begin(); iter != listData.end(); ++iter)
cout << *iter << endl;
}
Func({ 1, 2, 3 });
initializer_list的特性
- 它是一个轻量级的容器类型,内部定义了iterator等容器必须的概念。
- 对于initializer_list而言,它可以接受任意长度的初始化列表,但要求元素必须是同类型T(或者可转换成T)
- 它有三个成员接口:size(),begin(),end()
- 它只能被整体初始化或赋值
initializer_list<int> listData;
size_t nSize = listData.size(); // 0
listData = { 1, 2, 3 };
nSize = listData.size(); // 3
listData = { 1, 2, 3, 4, 5 };
nSize = listData.size(); // 5
initializer_list是非常高效的,它的内部并不负责保存初始化列表中的元素的拷贝,仅仅存储了列表中元素的引用而已。所以不能向如下使用:
initializer_list<int> Func()
{
int nA = 1;
int nB = 2;
return {nA, nB};
}
能够正常通过编译,但无法传递出我们希望的结果(nA,nB在函数结束时,生存期也结束,因此返回的将是不确定的内容)。
总结:我们应该总是吧initializer_list当成保护对象的引用,并在它持有对象的生存期结束之前完成传递。
2.3 列表初始化可以防止类型收窄
类型收窄包括如下几种情况:
- 一个浮点隐式转换为一个整数
- 从高精度浮点数隐式转换为低精度浮点数
- 从一个整数隐式转换为一个浮点数,并且超出浮点数的表示范围
- 从一个整数隐式转换为另一个长度较短的整数,并且超出了长度较短的整数的表示范围
int i = 2.2;
// double->float
float x = (unsigned long long)-1;
char y = 65536;
int a = 1.1; // Ok
int b = { 1.1 }; // error
3 for循环
3.1 for_each和foreach的用法
#include <QCoreApplication>
#include <iostream>
#include <algorithm>
using namespace std;
void DoCount(int& nCount)
{
cout << "Count = " << nCount << endl;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
vector<int> vecData = { 1, 2, 3, 4, 5 };
for_each(vecData.begin(), vecData.end(), DoCount); // 传入一个函数指针
return a.exec();
}
vector<int> vecData = { 1, 2, 3, 4, 5 };
int nVal = 0;
foreach (nVal, vecData)
cout << "Val = " << nVal << endl;
3.2 for循环新用法
vector<int> vecData = { 1, 2, 3, 4, 5 };
for (auto nVal : vecData)
cout << "Val = " << nVal << endl;
/**********************************************/
void ShowMap(const map<int, string>& mapData)
{
for (const auto& pairData : mapData)
cout << "first:" << pairData.first << "---second:" << pairData.second << endl;
}
4 lambda表达式
void SomeFunction(const vector<int>& vecData)
{
vector<int>::const_iterator pFind = find_if(vecData.begin(), vecData.end(), [](int nValue) { return (1 == nValue % 2); });
cout << *pFind << endl;
}
int main()
{
vector<int> vecData = { 1, 45, 6, 5, 8 };
SomeFunction(vecData);
system("pause");
return 0;
}
5 移动构造函数
5.1 左值和右值
5.1.1 左值
左值就是能用在赋值语句等号左侧的内容(它得代表一个地址)。
5.1.2 右值
右值就是不能作为左值的值,右值不能出现在赋值语句中等号的左侧。
5.1.3 左值和右值特性
C++中的一条表达式,要么是右值,要么就是左值。一个左值可能同时具有左值属性和右值属性。如:i = i + 1;i出现在等号的左右两侧,在等号的右侧,用的是对象的右值属性。左值表达式就是左值,右值表达式就是右值。左值代表一个地址,所以左值表达式求值的结果就是一个对象,得有地址。
5.1.4 常见用到左值的运算符
- 赋值运算符"="
- 取地址运算符"&"
- string,vector等的下标运算符"[]"
- 自加,自减运算符(–,++)
int nA = 5;
(nA = 5) = 8; // 赋值运算符,最终结果 nA = 8;
&nA; // 取地址运算符,返回一个地址 &123这种写法不成立
string str = "I Love Changde";
str[0]; // []运算符 "abc"[0]这种写法不成立
nA++; // 自加运算法 9++这种写法不成立
5.2 引用
5.2.1 引用的分类
- 左值引用
- const引用(常量引用)
- 右值引用(绑定到右值)
310

被折叠的 条评论
为什么被折叠?



