int main()
{
int num{ '\n' };
std::cout << num << std::endl;
std::cout << int{ '\n' } << std::endl; //< 输出:10
std::cout << int{ '\12' } << std::endl; //< 输出:10
std::cout << int{ '\x0A' } << std::endl; //< 输出:10
system("pause");
return 0;
}
【关键好用】
u8“12345”,类型char,使用utf-8,每个字符串ascii码下为1字节,其他如中文编码采用不定长字节,以\0结尾。如:u8"中",为4个字节(“中”3个字节 + '\0’1个字节)。初始化的语法较特殊,和赋值的语法完全两码事
特性:使用花括号“{}”对变量进行初始化,不限定变量类型
限制:当对内建类型初始化时,如果编译检测有数据丢失风险,会报错,如
int a{0.12f}; \\< 编译错误
报错
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestCpp11.cpp(10,24): error C2397: 从“float”转换到“int”需要收缩转换
列表初始的定义:
{ initializer-list }:初始化列表,可嵌套使用,如,{ {1, 2}, {3, 4} }
{ designated-initializer-list }(C++20):仅适用于结构体、聚合类型,可指定元素初始化,如,{.m=2, .n=3}
这里就引申出来,对自定义类型,如何进行列表初始?如下:
class CTestConstruct
{
public:
CTestConstruct(int nNum, std::string strMsg)
: m_nNum(nNum)
, m_strMsg(strMsg)
{
printf("%s,%d: msg,%s, num,%d\r\n",
__FUNCTION__, __LINE__, m_strMsg.c_str(), m_nNum);
}
private:
int m_nNum;
std::string m_strMsg;
};
int main()
{
CTestConstruct obj{ 3, "33" };
CTestConstruct arrObj[2] = { {1,"11"}, {2, "22"} }; //< 相当对数组每个元素,再执行列表初始化
typedef struct
{
int x;
int y;
}STU_DATA;
STU_DATA stdData{ .x = 4, .y = 44 };
system("pause");
return 0;
}
定义
:空指针不指向任何对象,其值 = 0,C++新标准中使用 nullptr表示
int *p1 = nullptr; //< 等价于 int *p1 = 0;
int *p2 = 0;
int *p3 = NULL;
注意
:尽量避免使用NULL,这个值属于预定义变量而并非关键字,其值在不同系统中可能不同
二维数组
C++语言中并没有真正的多维数组,所谓的多维数组,实际是数组的数组
如下定义
int a[][3] = {{1, 2, 3}, {2, 3, 4}}
a 首元素的本质是一个一维数组,即一维指针,数组元素的类型 是int[3]。
二维数组的第二维是数组元素类型的一部分,所以不能省略。
以上定义等价于
typedef arr_item int[3];
arr_item *a[] = {{1, 2, 3}, {2, 3, 4}};
由于a是一维数组,数组元素大小为 sizeof(int[3]),步长为3,大小12个字节。
其内存结构是连续的,如下
二维指针
二维指针的定义是指向指针的指针,和二维数组(本质是一维指针)存在本质的不同。
如下定义
int** pA = new int*[2];
pA[0] = new int[3];
pA[1] = new int[3];
pA是一个二维指针,指向一个指针数组。
数组元素大小为sizeof(int *),步长为1,64位下大小4个字节。
第二维的指针不连续,内存结构如下
被称为万能引用
如:
int i = 10;
const int ci = 100;
const int &i1 = i;
const int &i2 = 10;
const int &i3 = ci;
这个在函数参数中就很有用了
int func(const int ¶m);
int func2(int ¶m);
int i = 10;
const int ci = 100;
func2(i);
func2(10); //< 编译错误,使用常量
func2(ci); //< 编译错误,使用常量
//! 而以下,都能编译通过
func(i);
func(100);
func(ci);
constexpr用于修饰一个常量表达式(必须在编译阶段被识别),在以下情况可修饰函数:
在修饰变量时,包含有const的功能,二者区别不大,个人倾向于继续用const,觉得没必要去纠结太细微的区别。
constexpr 拓宽了常量表达式的范围,类似模板元编程,其定义的常量表达式,甚至是函数,可直接用于需要常量的运算,如:
constexpr int getSize1(int nNum)
{
return nNum * nNum;
}
const int getSize2(int nNum)
{
return nNum * nNum;
}
int main()
{
const int nNum = 2;
int arr1[getSize1(nNum)] = { 0 }; //< 编译通过
int arr2[getSize2(nNum)] = { 0 }; //< 错误
system("pause");
return 0;
}
//! 输出
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(218,20): error C2131: 表达式的计算结果不是常数
观点
:这个特性在算法、嵌入式、底层基础库等注重效率的地方,存在一定的好处;但在更注重业务逻辑的业务层,如界面,上层业务,基本没用。
C++11 引入了新的类型别名方式,用处不大,只是更“现代化”
using VCT_INT = std::vector;
其等价于
typedef std::vector VCT_INT;
这真是个好东西,对C++的提升简直意义重大,省去不少类型定义的烦恼
如:
typedef std::vector vctInt;
std::vector::iterator it = vctInt.begin();
就可以简单的写成
typedef std::vector vctInt;
auto it = vctInt.begin();
对于STL更复杂的定义,简直不要太方便
注意
:
auto i = 0, j = 1; //< 正确
auto a = 1. b = 1.1; //< 错误,类型不一致
auto *x = &i, y = i; //< 正确,auto等价于int,而定义出的x是int*,y是int类型
auto i = 1;
,编译器会判定i为int类型,而不是long,也不是int64,其原因是C++内部的隐式类型转换。而由于C++支持强制类型转换,理论上将上面的auto替换为其他类型都是能编译通过。这是对auto的一个补充特性
decltype(f()) x = v; //< x的类型取决于f()的返回值推导(发生在编译期),而不是v
auto y = v; //< 使用auto,则y的类型取决于v
比较有用的场景,函数模板的size_type,常规代码
VCT_INT vctInt;
for (std::vector::size_t i = 0; i < vctInt.size(); ++i)
...
使用decltype,可以优化为
VCT_INT vctInt;
for (decltype(vctInt.size()) i = 0; i < vctInt.size(); ++i)
...
不需要关注 vctInt.size() 的类型,避免警告发生,减少了代码信息量,扩展性还有所提升。
vector的值会动态增长(其他很多stl容器也一样),在增、删vector元素时,均可能导致相关迭代器失效,如:
for (auto it = vct.begin(); it != vct.end(); ++it)
{
if (*it == 1)
{
vct.erase(it); //< 导致后续的++操作异常
}
}
C++11中对指针增加了 begin(), end()
函数,用于确定常量指针的起始、结束位置。
98的感觉,迭代器的方法是仿造指针实现的
现在C++11,丰富了指针的方法,迭代器有的方法,普通指针都有了,指针反过来成为了迭代器的一种
这应该是 for (auto i : arr) 得以实现的基础
参考代码:
int p[100] = { 0 };
int* pBegin = std::begin(p);
int* pEnd = std::end(p);
printf("%lld == %lld\r\n", pEnd - pBegin, sizeof(p) / sizeof(int)); //< 输出100==100
早期版本:允许结果为负数的商向上或向下取整
C++11新标准规定:商一律向0取整,即直接切除小数部分
对bool 类型的比较需要注意,一般会作为代码规范进行规定
注意:
以下两个表达式的区别,特例 nValue=2
//! 假定nValue是int类型
//! 以下运算,将nValue转换为 bool 类型
//! nValue == 0,为false,nValue != 0,为true
if (nValue)
{
printf("ok");
}
//! 以下代码,会将 true 转换为 nValue 的类型进行比较
//! 即 nValue == 1,为true,nValue != 1,为false
if (nValue == true)
{
printf("ok");
}
int num = ~'q' << 6;
char c = ~'q' << 6; //< 会报警告,int类型转换为char类型
printf("0x%X\r\n0x%X\r\n", num, c);
//! 输出:
//! 0xFFFFE380
//! 0xFFFFFF80
C++新特性,语法定义
for (declaration : exporession)
statement
exporession:定义序列对象,即包含 begin()/end(),以及对应迭代器
declaration:定义一个变量,其类型可以是序列成员,也可以是通过序列成员转换获得
常规用法
for (auto item : v)
statement
其等价于
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg)
statement
注意
:
函数如果要使用可变形参,可以使用 “…”。
如果所有形参都是同一类型,可以使用stl类 std::initializer_list 作为参数,如
bool func(int nValue, std::initializer_list ilParam)
{
for (auto &item : ilParam)
{
printf("%s\r\n", item.c_str());
}
return true;
}
//! 调用
func(1, { "1", "2", "3" });
问题
:为什么要专门定义这样的一个类,直接用std::vector 或者 std::list不行么?从功能角度,bool func(int nValue, std::vector
解释
:这是C++11、编译器的一个约定,如下代码
std::vector vct = { "1", "2", "3" };
等价于
std::initializer_list ilst({"1", "2", "3"});
std::vector vct(ilst);
编译器对花括号初始化列表,会创建一个 initializer_list
_CONSTEXPR20 vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
参考:https://stackoverflow.com/questions/14414832/why-use-initializer-list-instead-of-vector-in-parameters
返回一个值的方式和初始化一个变量或形参的方式完全一样。
函数返回值,会创建一个临时变量(执行拷贝动作),用于赋值调用,当调用结束,该变量也将被销毁。
函数返回引用,则该引用仅是它所引用对象的一个别名,也即不存在临时变量一说。
class CMyObject
{
public:
CMyObject()
{
printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
}
CMyObject(const CMyObject& obj)
{
printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
}
~CMyObject()
{
printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
}
void print() const
{
printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
}
};
void funCall(const CMyObject& obj)
{
obj.print();
}
CMyObject getObject()
{
CMyObject obj;
return obj;
}
//! 调用
funCall(getObject());
//! 输出
[000000BAAD8FFAB4]CMyObject::CMyObject, 22
[000000BAAD8FFCB4]CMyObject::CMyObject, 26
[000000BAAD8FFAB4]CMyObject::~CMyObject, 30
[000000BAAD8FFCB4]CMyObject::print, 34
[000000BAAD8FFCB4]CMyObject::~CMyObject, 30
//! 对象创建、销毁顺序
1. getObject. “CMyObject obj;”,创建 000000BAAD8FFAB4、
2. getObject 退出后,执行拷贝,创建临时变量 000000BAAD8FFCB4
3. getObject 退出后,拷贝结束后,销毁函数内变量 000000BAAD8FFAB4
4. funCall 使用引用,此时未进行拷贝,直接调用变量 000000BAAD8FFCB4
5. funCall 执行结束后,销毁临时变量 000000BAAD8FFCB4
所以,特别注意
:不要返回局部对象的引用或指针,因为当函数结束时,该变量已经被销毁,即对应的引用和指针都将失效,而导致异常。
定义:定义在类内部的成员函数是自动inline的;其余在外部定义,使用inline修饰,足够小且简单,由编译器认可的为内联函数。
作用:在每个调用节点内联展开进行调用
优点:减少函数调用出栈入栈的开销,提升函数调用效率
缺点:增加编译代码大小
如以下函数的调用
inline const string& shorterString(const string& s1, const string& s2)
{
printf("shorterString\r\n");
return (s1.length() > s2.length()) ? s1 : s2;
}
int main()
{
string s1("12"), s2("234");
cout << shorterString(s1, s1) << endl;
system("pause");
}
main中调用内联函数,等价于
cout << (printf("shorterString\r\n"), (s1.length() > s2.length()) ? s2 : s1) << endl;
注
:内联函数在windows/VS编译器下,应用开发,使用较少,原因是VS的编译器对编译产物优化的很多,甚至没进行内联的,也给你搞成内联调用
默认构造函数,即类控制执行默认的初始化过程的函数,该函数没有任何参数。
当且仅当类没有显示声明任何构造函数时,编译器为该类隐式定义默认构造函数,称为合成的默认构造函数。
举例
class CA {};
CA obj;
class CB
{
pubic:
CB() = default;
CB(int param) {}
};
CB objB;
以上,类CA,编译器会创建默认构造函数,可以直接定义obj。
类CB,虽然类已经定义了其他构造函数,但使用了= default
,编译器会继续自动生成默认构造函数,default在内部,则生成内联默认构造函数;在类外部,则该函数就不是内类的。
对比不带默认构造函数的方式:
class CC
{
public:
CC(int param){}
};
CC obj(123);
以上,类CC,定义了带参数的构造函数,也没用= default
要求编译器自动生成默认构造函数,声明类对象时,必须调用对应构造函数,带上具体的参数。
注
:正常情况,我们并不期望出现默认构造函数,因为这会使成员函数初始化成不可知的值,导致未定义行为。
关键字:mutable
定义:该成员变量永远不会是const,就算包含它的类对象是const,或使用它的函数是const,该成员仍能被改变。
举例,以下代码是合法的:
#include
#include
#include
class CObject
{
public:
void setString(const std::string& str) const
{
//! 在const 成员函数里,修改成员变量
m_strData = str;
printf("%s\r\n", m_strData.c_str());
}
private:
mutable std::string m_strData;
};
int main()
{
CObject obj;
obj.setString("abc"); //< 打印"abc"
const CObject cstObj; //< const对象中,修改成员
cstObj.setString("123"); //< 打印"123"
system("pause");
return 0;
}
去除mutable,如下代码报错
#include
#include
#include
class CObject
{
public:
void setString(const std::string& str)
{
//! 在const 成员函数里,修改成员变量
m_strData = str;
printf("%s\r\n", m_strData.c_str());
}
private:
std::string m_strData;
};
int main()
{
const CObject cstObj; //< const对象中,修改成员
cstObj.setString("123"); //< 打印"123"
system("pause");
return 0;
}
报错内容
已启动生成...
1>------ 已启动生成: 项目: TestCpp11, 配置: Debug x64 ------
1>TestClass.cpp
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(51,24): error C2662: “void CObject::setString(const std::string &)”: 不能将“this”指针从“const CObject”转换为“CObject &”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(51,2): message : 转换丢失限定符
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(37,7): message : 参见“CObject::setString”的声明
1>已完成生成项目“TestCpp11.vcxproj”的操作 - 失败。
========== “生成”: 0 成功,1 失败,0 更新,0 已跳过 ==========
注
:这个关键字应用场景太少,使用这个关键字的场景,是否是设计不合理导致的?
类的数据成员初始化有3种,初始化顺序 3 --> 2 --> 1
第3种初始化方案,这个特性在C++11到C++20变化较大,特殊考虑静态成员、非静态成员和兼容C++98 静态整形常量成员初始化语法,同时针对静态常量成员,增加了内联数据成员(用const static inline 修饰)
以下代码比较绕:
class CMyClass
{
double m_a = 1.0; //< 成功
double m_b = 2.0; //< 成功
static double m_c = 3.0; //< 失败,不允许初始化静态成员变量
const double m_d = 4.0; //< 成功
const static double m_e = 5.0; //< 失败,不允许初始化静态成员变量
static int m_f = 6; //< 失败,不允许初始化静态成员变量
const static int m_g = 7; //< 成功,兼容C++98标准,允许初始化静态int成员变量
static inline double m_h = 8.0; //< 成功
const static inline double m_i = 9.0; //< 成功
};
报错信息如下
已启动生成...
1>------ 已启动生成: 项目: TestCpp11, 配置: Debug x64 ------
1>TestClass.cpp
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(64,25): error C2864: CMyClass::m_c: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(64,25): message : 类型是“double”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(66,31): error C2864: CMyClass::m_e: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(66,31): message : 类型是“const double”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(67,20): error C2864: CMyClass::m_f: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(67,20): message : 类型是“int”
1>已完成生成项目“TestCpp11.vcxproj”的操作 - 失败。
========== “生成”: 0 成功,1 失败,0 更新,0 已跳过 ==========
这里记录一个特殊的情况,后续在仔细查下
#include
#include
#include
std::string getString(int nNum)
{
printf("%s:%d, %s, %d\r\n", __FILE__, __LINE__, __FUNCTION__, nNum);
char szNum[1024] = { 0 };
sprintf_s(szNum, sizeof(szNum), "%d", nNum);
return szNum;
}
class CMyClass
{
public:
CMyClass()
: m_strB(getString(321))
{
printf("%s:%d, %s\r\n", __FILE__, __LINE__, __FUNCTION__);
}
private:
std::string m_strB{ getString(123) };
};
int main()
{
CMyClass obj;
system("pause");
return 0;
}
注意
:以上代码,在VS2022 debug C++17下,getString只执行了一次,输出如下,后续需要再进一步深入其原理
。网上查到的信息(未考证),描述编译器最终会在构造函数初始化列表中实现内部初始化,即重复时,构造函数初始化列表覆盖内部初始化代码。
E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp:106, getString, 321
E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp:118, CMyClass::CMyClass
请按任意键继续. . .
一直以来我都是排斥友元的,我认为它破坏了类的封装性,让完好自洽的逻辑变得混乱。但还是有极特殊的场景会使用到,比如Qt中 QxxxPrivateData 的设计,将数据和业务分成两个对象进行管理,使用友元使类之间实现有限访问,不过我还是觉得这种设计是一种偷懒行为,并不是成熟代码。
这里还是对友元的语法进行记录,方便后续查阅。冷门的语法,不用就容易忘。
类友元函数
class CScreen
{
//! 使能访问本类的私有成员
friend void PrintSrceen(CScreen& obj);
private:
void print()
{
printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
}
int m_nWidth = 10;
int m_nHeight = 10;
};
void PrintSrceen(CScreen &obj)
{
obj.print();
printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
}
输出:
CScreen::print, 10x10
PrintSrceen, 10x10
类友元类
class CScreen
{
//! 使能访问本类的私有成员
friend class CPrinter;
private:
void print()
{
printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
}
int m_nWidth = 10;
int m_nHeight = 10;
};
class CPrinter
{
public:
void PrintSrceen(CScreen& obj)
{
obj.print();
printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
}
};
int main()
{
CScreen obj;
CPrinter objPrinter;
objPrinter.PrintSrceen(obj);
system("pause");
return 0;
}
输出:
CScreen::print, 10x10
CPrinter::PrintSrceen, 10x10
类友元类的成员函数
这里使友元的访问范围更严格,同时声明起来有点绕
//! 1. 提前声明,确保CPrinter::PrintSrceen声明通过
class CScreen;
//! 2. 声明必须在CScreen声明友元之前,否则友元的声明非法
class CPrinter
{
public:
void PrintSrceen(CScreen& obj);
};
class CScreen
{
//! 3. 使CPrinter中特定的函数,访问本类的私有成员
friend void CPrinter::PrintSrceen(CScreen& obj);
private:
void print()
{
printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
}
int m_nWidth = 10;
int m_nHeight = 10;
};
//! 4. 实现必须在CScreen声明友元之后,否则函数的定义非法
void CPrinter::PrintSrceen(CScreen& obj)
{
obj.print();
printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
}
之前把操作符重载想复杂了,看到这么多参数,没有什么一致性就头疼,现在想想其实还是简单的,还是在通用规则的框架下。
首先
,操作符重载有以下限制
1、不能重载的操作符
域限定符 ::
直接成员访问操作符 .
三目运算符 ?:
字节长度操作符 sizeof
类型信息操作符 typeid
2、重载操作符不能修改操作符的优先级
3、无法重载所有基本类型的操作符运算
4、不能修改操作符的参数个数操作数
5、不能发明新的操作符
6. 只能作为成员函数重载:= 赋值运算符, []下标运算符, ()函数调用运算符, ->成员访问运算符,且是非静态成员,不能友元
其次
,操作符重载,当成普通函数即可,仅限制了操作符的参数个数、返回类型,具体参数类型无所谓,设置返回类型也可再一定程度上忽略。
比如:
bool operator==(const std::string &left, const std::string &right);
写成
int operator==(const std::string &left, const char *szRight);
也是可以的,只是==操作符要求,要两个元素比较,同时返回比较结果,我这返回的是int,也是合理。
再次
,操作符重载分全局、成员,全局就需要写出所有的操作对象,而成员操作符重载,已经提前放入了一个参数,就是当前对象。
以上"=="操作符可改写为
int operator==(const char *szRight);
最后
,补充,没必要的话,我们就不要重载一些奇怪的操作符,如&& || &,因为在C++中已经定义了其对类对象的操作, 重载该运算符会导致丧失一部分功能, 为类的使用者带来麻烦。正常的重载,理论上不会有什么问题。
C++默认赋值操作,即operator=,如A=B,会将B的成员(非static)逐一赋值给A的成员,调用=号运算符。
但是,仅仅是赋值,针对指针类型,也只是赋值指针的值,而不对指针进行拷贝。
所以
:我们一般不适用默认赋值操作,需要显示定义operator=操作符,不过我们一般再定义个自定义函数名,使其意义更明确,比如A.copy(B)。