文章目录
- 前言
- C++ Primer 基础部分学习笔记
- Chapter I
- Chapter II
- Chapter III
- **3.1.0: using的使用**
- **3.2.3: String**
- **3.2.3: ==C++11==范围for循环**
- **3.3.0: vector容器**
- 3.4: C++ STL 迭代器
- 3.5.2: 下标访问元素
- 3.5.3: 标准库函数begin()&end()
- 3.5.3: ptrdiff_t类型
- 3.5.5: C++工程代码优化
- Extra拓展知识: 原生字符串(RawString)
- Chapter IV
- Chapter V
- Chapter VI
- **6.1.2: 关于函数声明问题**
- **6.2.2: 关于函数的引用传递**
- **6.2.6: C++中可变形参的函数**
- **6.3.2: 关于函数返回值**
- **6.3.2: 函数返回引用或指针时**
- **6.3.2: 关于递归**
- **6.3.3: 返回数组指针的函数**
- **6.4.1: 关于函数重载的注意点**
- **6.4.2: 函数重载和const**
- **6.4.1: 函数重载与作用域**
- **6.5.1: 关于函数参数的缺省值**
- **6.5.2: constexpr函数**
- **6.5.3: 关于断言库assert**
- **6.5.3: DEBUG辅助工具**
- **6.6.0: 函数重载的匹配**
- **6.7: 函数指针**
- Chapter VII:
前言
本篇博客作为C++ Primer的复习笔记与快速查阅手册使用, 全篇阅读困难较大
C++ Primer 基础部分学习笔记
只有将每一个知识点&功能和实际使用中的具体需求一一绑定, 才能算作是真正学会了
注意在C++程序设计中, 不提倡使用带.h的头文件, 最好使用C++转化后的头文件, 如cstdio, 带.h的头文件有时会引发一些冲突
C++转化后的头文件是将末尾的.h删去, 并在开头添加一个c, 表明此头文件继承自C语言, 且将文件内部定义的标识符划入namespace std中, 而.h的头文件则不然
如果在C++程序中使用.h头文件, 则程序员不得不时刻牢记哪些是从C语言中继承的, 而哪些又是C++特有的, 而使用cname的头文件时, 标准库中的名字总能在namespace std中找到, 更方便与稳定
Chapter I
1.2.1:
与C语言的stdout, stdin, stderr 相对的 C++也定义了相关的标准输入输出: cout & cerr & cin & clog(这个为输出程序运行的log日志)
1.2.2:
C的=运算符返回右值, 而
<< >>(流输入&流输出) 返回的是左值, 而且是从左往右执行的, 所以可以用 cin>>a>>b (cin>>a返回的是cin)
1.2.3:
cout<<endl 与 \n类似, 会强制刷新缓冲区
1.2.4 使用using namespace std;的作用:
因为c++标准库定义的所有名字都在std命名空间中, 如果没有使用using namespace std; 则需要用作用域运算符::来单独标志
(C++使用命名空间来防止标识符冲突, 不同的命名空间中可以使用相同的标识符)
但是:
- 在工程中不提倡使用using namespace, 最好是使用::作用域运算符来单独标记, 因为工程中通常使用多个文件, 不同的文件相互#include时很容易出错
- 不应在头文件中包含using namespace指令, 这是素质吊差的行为
Chapter II
2.1.1: 扩展char类型:
在基本字符类型char的基础上, C++提供了扩展字符类型作用于扩展字符集:
- wchar_t 宽字符类型, 确保可以存放机器最大扩展字符中的任意一个字符。
- char16_t & char32_t 为Unicode字符集服务(因为Unicode用于表示所有自然语言, 所以需要的储存空间更大)
2.1.1: 编程习惯养成
- 当明确知晓数值不可能为负时, 选用无符号类型
- 不要在算术表达式中使用char或bool类型, 因为char在有些机器上是有符号的, 而有些机器则是无符号的, 除非使用signed char或unsigned char来替代
- 浮点计算通常使用double, float基本废弃
2.1.2: 混用signed & unsigned:
混用signed & unsigned的表达式中, signed类型的对象会被自动转化为unsigned, 此时会导致出现神奇错误
2.1.3: 字符与字符串字面量的前缀
例如:
L'a' //wchar_t
u8"hi!" //utf-8
U"WDNMD" //char32_t
2.2.1: 变量的初始化
除了常见的=初始化, C++11还提供了括号初始化和列表初始化:
- 括号初始化: 特殊的初始化方法, 相对于赋值运算符而言, 括号赋值只能用在变量初始化中
如:
double temp(0.0001);
- 列表初始化: C++11 Only , 采用大括号, 特点是当初始值存在信息丢失风险,则编译器告警, 如 int temp={0.0032}; //编译器告警
两种形式:
double temp={0.0032}; //可用在任何一处, 包括对struct & class的初始化
double temp{0.0032}; //只能用在初始化中
2.2.4: 在块作用域中访问同名的全局作用域变量
使用没有前缀的::作用域运算符, 如:
#include <iostream>
using namespace std;
int x=6; //全局变量
int main( ){
int x=1;
cout<<x<<endl; //访问局部变量
cout<<::x<<endl; //访问全局变量
}
输出结果为
1
6
但是绝对不建议定义同名的变量, 这样极易引起误解
2.3.1: 引用变量
前排提醒: C++中的引用在汇编层的实现实际上是编译器用指针实现的, 但是这个实现在语言层面对程序员做了透明化处理
- 引用变量的类型都要和与之绑定的对象严格匹配, 并且只有被const修饰的引用变量才能引用字面量或是表达式的值
当引用对象与其绑定的对象类型不匹配时:
double dval=3.14;
const int &ri=dval;
编译器相当于做了如下处理:
const int temp=dval;
const int &ri=temp;
此时输出ri只会得到个3, 但是当没有用const修饰ri时:
double dval=3.14;
int &ri=dval; //编译器判断Error, 非法
- 一旦定义了引用,就无法令其再绑定到其他对象上
- 因为引用本身不是一个对象, 因此不能定义指向引用的指针, 但是可以定义对指针的引用
如:
const int &r1=42;
const int &r2=r1*2;
r2=n1 //这是赋值
int *p;
int *&r=p; //r是对指针p的引用
//首先从右往左阅读, 第一是&r, 代表r是一个引用, 而后是*&r, 代表r是对一个指针的引用
2.3.2: 空指针の初始化
int *p1=nullptr;
int *p2=0;
int *p3=NULL;
//三个定义等价
nullptr 是C++11引入的一个方法, 这是一个特殊类型的字面量, 用nullptr定义的指针可以被转化成任意其它类型的指针变量, 同时, 在支持C++11的新标准下, 最好使用nullptr而避免使用NULL, 因为:
传统意义上来说,c++把NULL、0视为同一种东西,有些编译器将NULL定义为 ((void*)0),有些将其定义为0.
c++不允许直接将void* 隐式的转化为其他类型,但是如果NULL被定义为 ((void*)0),
当编译char* p = NULL;
NULL只好被定义为0。
还有:
void func(int);
void func(char *);
如果NULL被定义为0,func(NULL)会去调用void func(int),这是不合理的
所以引入nullptr,专门用来区分0、NULL。
2.3.2: void*
相当于一种通用指针, 特点如下:
5. 将其赋给任意类型的指针完全不用考虑类型匹配的问题, 但c++不允许直接将void* 隐式的转化为其他类型, 只能使用显示转化
6. 不能直接操作指针所指的对象, 因为编译器不知道指向的对象是什么类型?
2.4.0: const在多文件中的应用
默认状态下, const对象仅在相应文件内有效, 当多个文件中的变量时,等同于在不同文件中分别定义了独立的变量
但是当需要在多个文件中使用同一个const变量时, 需要在对应的const变量的声明和使用处都添加extern修饰符, 如:
// file1.cpp
extern const int bufSize=fcn();
//file2.cpp
extern const int bufSize;
//其中第一个带有赋值的是定义, 而后无赋值的是引用
//file1中的extern表明bufSize可以被外部使用(外部链接)
//file2中的extern表明让编译器到别处去寻找bufSize变量
2.4.3: 顶层const & 底层const
-
顶层const, 指的是此const修饰的数据对象不能被修改
如: int *const ptr; ptr指向的对象是int类型, 可以被修改, 但是ptr不可指向其他东西 -
底层const, 指的是此对象指向的对象不能被修改, 而其本身是可被修改的,
如const int *ptr; ptr指向的对象为const int类型, 不可被修改, 但是ptr本身可以指向其他东西
快速准确的判断方法就是看const离哪个比较近:
顶层const离ptr比较近, 故修饰ptr指针
底层const离int比较近, 故修饰int指向对象
2.4.4: constexpr关键字
constexpr用于修饰在编译期可求值的对象, 即该对象是一个常量表达式, 对象被修饰后与const类型相似, 在后头的程序中不可被修改, 即其变成一个常量(字面量)
最常见的常量表达式就是字面值或全局变量/函数的地址或sizeof等关键字返回的结果, 而其它常量表达式都是由基础表达式通过各种确定的运算得到的
在大型程序中, 通常很难确定一个表达式是否是常量表达式, 用constexpr修饰可以很好的表达语义
constexpr的好处:
- 是一种很强的约束,更好地保证程序的正确语义不被破坏, 即:
double dval=0.123;
constexpr int te1=9; //编译正确, 9为一个常量表达式
constexpr int te2=dval; //编译错误, dval不是常量表达式, 在编译期无法被求值
//只有在程序运行时才会被附上初始值
-
编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
-
相比宏(#define等)来说,没有额外的开销,但更安全可靠。
特殊情况:
- 用constexpr修饰指针时, constexpr只对指针有效, 而对其指向的数据对象无效, 且修饰的指针值必须是NULL或0或nullptr
2.5.2: auto修饰符
特殊情况: 当auto与引用联用时, 判断的是被引用对象的类型, 且通常会忽略掉顶层const, 同时保留底层const:
const int ci = i, &cr = ci;
auto b=ci; //b是int类型, 忽略了ci的const属性
auto c=cr; //c是int类型, 忽略了ci的const属性
auto d=&i; //d是指向int的指针
auto e=&ci; //e是指向const int的指针
auto通常会忽略顶层const, 保留底层const
如果希望auto保留顶层const, 需要在前头添加const:
const auto f=ci;
同时auto只会返回对象的类型, 不会返回对象的指针
2.5.3: decltype修饰符
与auto配套, auto用于推断数据对象的类型, 而decltype用于推测表达式的类型:
-
用于推测表达式的类型
有时我们希望从表达式的类型推断出要定义的变量类型,但是不想用该表达式的值初始化变量(如果要初始化就用auto了)。为了满足这一需求,C++11新标准引入了decltype类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。double f(); decltype( f()) sum=0; //推导出sum为double int i=42; double d=3.14; decltype(i+d) e; //推导出i+d类型为double
特殊属性:
-
与auto不同的是, 当decltype使用的表达式是一个变量是, 会返回变量的完整类型(包括引用和顶层const)
-
如果decltype的表达式是一个解引用, 如(*ptr), 则得到的会时一个引用类型, 如:
int *ptr=i; decltype (*ptr) te; //te是对int的引用, 必须赋初值
-
如果decltype的表达式加上了括号, 得到的也是引用
因为如果使用的是一个不加括号的变量,得到的结果就是该变量的类型, 但是如果对变量加上了一层或多层括号, decltype会将其当成一个表达式, 而一个变量在表达式中是一种可以作为赋值语句左值的特殊表达式, 所以最后会得到该变量类型的引用int i=99; decltype ((i)) p; //p是对int类型的引用, 必须赋初值
-
对于用decltype推断函数类型, 加括号得到的是函数的返回值类型, 不加括号得到的是函数类型
int *funcTest(int &m); int i=0; decltype(funcTest) val1; //funcTest是一个函数, 即相当于一个变量, 类似decltype(i), 返回的是i的类型 //此处判定出的是funcTest的类型, 即int * (int &) //所以val1为返回int * (int &) 的函数 decltype(funcTest(i)) val2; //funcTest(i)为对函数的调用, 即相当于一条语句 //判断出的是函数的返回值类型, 所以val2是int *
对比auto:
-
auto忽略顶层const而保留底层const,decltype保留顶层和底层const;
-
对引用操作,auto推断出原有类型,decltype推断出引用
int i = 12345 ,&r = i; auto autoObj = r; //autoObj为int decltype(r) declObj = r; //declObj为int的引用 autoObj++; cout<<i<<' '<<autoObj<<endl; declObj++; cout<<i<<' '<<declObj<<endl;
输出结果:
12345 12346
12346 12346 -
对解引用操作,auto推断出原有类型,decltype推断出引用;
-
auto推断时会实际执行,decltype不会执行,只做分析。
反正decltype会返回表达式完整的类型, 而auto会去掉引用和顶层const
2.6.1: 关于类定义时赋值
在定义类对象时附上初始值, 是C++11新标准引入的, 称为类内初始值, 创建对象时,类内初始值将用于初始化数据成员, 没有初始值的成员将被默认初始化。
注意: 类内初始化只能使用等号赋值或列表初始化, 不能使用括号初始化
Chapter III
3.1.0: using的使用
using在C++11中有多个用法:
命名空间
- 可以使用using namespace std; 对当前文件中的各处使用std命名空间的标识符
- 也可以单独使用 using namespaceXX::nameXX; 即文件中各处的nameXX均使用定义在namespaceXX中的nameXX
注意: 不应在头文件中包含using指令, 这是素质吊差的行为, 因为如果在头文件中使用using, 则所有引用这个头文件的文件都会有这个声明, 对于大型程序而言, 这会造成致命错误
别名指定(用于替代传统typedef)
使用方法:
using value_type = _Ty //对_Ty起了个别名value_type, 即使用value_type的地方相当于使用_Ty
与typedef的区别是, using有更好的可读性, 对于typedef, 复杂的别名经常需要花一定时间理解, 而using的语法中把别名强制分离到了左边,而把别名指向的放在了右边,且用等号区分, 比较清晰, 如:
typedef void (*FP) (int, const std::string&);
using FP = void (*) (int, const std::string&);
且有些功能必须使用using才能实现, 如 alias templates, 模板别名:
//使用using
template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;
// usage
Vec<int> vec;
//使用typedef
template <typename T>
typedef MyVector<T, MyAlloc<T>> Vec;
// usage
Vec<int> vec;
其中使用typedef是错误的, 会得到如下错误信息: error: a typedef cannot be a template
C++11中鼓励使用using而不是typedef
子类引用基类成员
(此Part暂时没学到)
假如子类私有继承父类,子类无法使用父类的成员(变量,函数等),但是使用using可以访问,如:
template<class _scale, int option>
class Person
{
public:
_scale age;
_scale height;
_scale name[option];
void myprint(void)
{
cout << "age" << age << endl;
}
};
template<class _scale, int option>
class HeighPerson : private Person<_scale, option>
{
public:
using Person<_scale, option>::age; // 使用using 之后变成public
using Person<_scale, option>::myprint; // 使用using, myprint之后变成public
protected:
using Person<_scale, option>::height; //在本来不可访问的,使用using 之后变成protected
void test(void)
{
cout << "age" << age << endl;
}
};
3.2.3: String
string的成员函数:
//默认构造函数:
string();
//拷贝构造函数:
string (const string& str);
//子串构造函数:
string (const string& str, size_t pos, size_t len = npos);
//C字符串构造函数:
string (const char* s);
string (const char* s, size_t n);
//填充构造函数:
string (size_t n, char c);
//范围构造函数(部分拷贝):
template <class InputIterator>
string (InputIterator first, InputIterator last);
//不定参数构造函数:
string (initializer_list<char> il);
//转移构造函数(这个先等等......)
string (string&& str) noexcept;
//--------------------------------
//iterator迭代器系列函数:
//返回首迭代器
iterator begin() noexcept;
const_iterator begin() const noexcept;
//返回尾迭代器
iterator end() noexcept;
const_iterator end() const noexcept;
//返回首反向迭代器
reverse_iterator rbegin() noexcept;
const_reverse_iterator rbegin() const noexcept;
//返回尾反向迭代器
reverse_iterator rend() noexcept;
const_reverse_iterator rend() const noexcept;
//返回以上类型的const迭代器
const_iterator cbegin() const noexcept;
const_iterator cend() const noexcept;
const_reverse_iterator crbegin() const noexcept;
const_reverse_iterator crend() const noexcept;
//-----------------------------------------
//Capacity容量系列函数:
//注意: 此类函数中: 如果超出了max_size, 则抛出lenth_error异常信息
// 如果申请内存失败, 则抛出bal_alloc异常信息
//返回string中储存的字符串的长度, 以字节为单位, 注意返回的不是其实际申请的内存大小
size_t size() const noexcept;
size_t length() const noexcept; //这俩一样
//返回string对象当前申请的内存大小, 以字节为单位
size_t capacity() const noexcept;
//返回string最大可申请到的内存大小, 以字节为单位
size_t max_size() const noexcept;
//设置string中储存的字符串的长度为n
//如果n小于当前长度, 则舍弃多余的字符
//如果n大于当前长度, 则用c初始化新的字符, 如果未给出c, 则使用默认初始化
void resize(size_t n);
void resize(size_t n, char c);
//设置string的capacity至少达到n
//当n>当前capacity时, 增加capac至n, 其他情况下, 不改变capacity的大小
//此函数不会修改string中储存的字符串
void reserve(size_t n = 0);
//清空string, 将size置零
void clear() noexcept;
//返回string是否为空的标志
bool empty() const noexcept;
//请求string缩减capacity到size
//此函数非强制指令, 编译器可以智能的调整capacity
void shrink_to_fit();
//----------------------------------------------------------
//Element access元素访问系列函数:
//直接下标访问, 如果下标越界, 将造成未定义行为
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
//函数下标访问, 如果下标越界, 将抛出out_of_range异常信息 (比直接访问更安全)
char& at(size_t pos);
const char& at(size_t pos) const;
//返回最后一个字符的引用, 注意当string为空时将造成未定义行为
char& back();
const char& back() const;
//返回第一个字符的引用, 注意当string为空时将造成未定义行为
char& front();
const char& front() const;
//----------------------------------------------------------
//Modifiers编辑器系列函数:
//注意: 此类函数中: 如果访问越界, 则抛出out_of_range异常信息
// 如果超出了max_size, 则抛出lenth_error异常信息
// 如果申请内存失败, 则抛出bal_alloc异常信息
// 如果迭代器失效, 将会造成未定义行为
//string拼接, 返回当前string对象的引用(即*this)
//重载运算符版本
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);
string& operator+= (initializer_list<char> il);
//成员函数版本, 返回*this
string& append(const string& str);
//如果subpos越界, 则抛出out_of_range异常信息
string& append(const string& str, size_t subpos, size_t sublen);
string& append(const char* s);
string& append(const char* s, size_t n);
string& append(size_t n, char c);
template <class InputIterator>
string& append(InputIterator first, InputIterator last);
string& append(initializer_list<char> il);
//向string末尾添加字符
void push_back(char c);
//拷贝赋值, 删除string中原有的全部字符, 返回*this
string& assign(const string& str);
string& assign(const string& str, size_t subpos, size_t sublen);
string& assign(const char* s);
//如果n大于数组s的长度, 则导致未定义行为
string& assign(const char* s, size_t n);
string& assign(size_t n, char c);
template <class InputIterator>
string& assign(InputIterator first, InputIterator last);
string& assign(initializer_list<char> il);
string& assign(string&& str) noexcept;
//在pos位置插入指定的字符/字符串
string& insert(size_t pos, const string& str);
string& insert(size_t pos, const string& str, size_t subpos, size_t sublen);
string& insert(size_t pos, const char* s);
//如果n大于数组s的长度, 则导致未定义行为
string& insert(size_t pos, const char* s, size_t n);
string& insert(size_t pos, size_t n, char c);
iterator insert(const_iterator p, size_t n, char c);
iterator insert(const_iterator p, char c);
template <class InputIterator>
iterator insert(iterator p, InputIterator first, InputIterator last);
string& insert(const_iterator p, initializer_list<char> il);
//删除pos位置指定数量的字符
string& erase(size_t pos = 0, size_t len = npos);
iterator erase(const_iterator p);
//如果迭代器无效, 则会导致未定义行为
iterator erase(const_iterator first, const_iterator last);
//替代从pos开始长度为len的字符串, 或是指定迭代器中的字符串
string& replace(size_t pos, size_t len, const string& str);
//i2还是尾迭代器
string& replace(const_iterator i1, const_iterator i2, const string& str);
string& replace(size_t pos, size_t len, const string& str,
size_t subpos, size_t sublen);
string& replace(size_t pos, size_t len, const char* s);
string& replace(const_iterator i1, const_iterator i2, const char* s);
string& replace(size_t pos, size_t len, const char* s, size_t n);
string& replace(const_iterator i1, const_iterator i2, const char* s, size_t n);
string& replace(size_t pos, size_t len, size_t n, char c);
string& replace(const_iterator i1, const_iterator i2, size_t n, char c);
template <class InputIterator>
string& replace(const_iterator i1, const_iterator i2,
InputIterator first, InputIterator last);
string& replace(const_iterator i1, const_iterator i2, initializer_list<char> il);
//交换当前string与str中的字符串
void swap(string& str);
//弹出最后一个字符, 如果字符串为空, 则将导致未定义行为
void pop_back();
//-----------------------------------------------
//String operations字符串操作系列函数:
//返回C原生字符串, 末尾绝对有\0
const char* c_str() const noexcept;
const char* data() const noexcept; //这个在C++98中不含\0, 而C++11中与C_str相同
//返回allocator空间配置器(这个先放着)
allocator_type get_allocator() const noexcept;
//将pos位置的长度为len的字符串拷贝到s, 不会再末尾加\0
//返回拷贝的字符的数量
//如果空间不够, 则会产生未定义行为
size_t copy(char* s, size_t len, size_t pos = 0) const;
//从pos开始查找字符/字符串(包括pos), 返回查找到的字符/字符串第一次出现的位置
//功能与strstr()相似
size_t find(const string& str, size_t pos = 0) const noexcept;
size_t find(const char* s, size_t pos = 0) const;
size_t find(const char* s, size_t pos, size_type n) const;
size_t find(char c, size_t pos = 0) const noexcept;
//从pos开始查找字符/字符串(包括pos), 返回查找到的字符/字符串最后一次出现的位置
//功能与strstr()相似
size_t rfind(const string& str, size_t pos = npos) const noexcept;
size_t rfind(const char* s, size_t pos = npos) const;
size_t rfind(const char* s, size_t pos, size_t n) const;
size_t rfind(char c, size_t pos = npos) const noexcept;
//下头这4组查找函数如果没有找到返回的都是string::npos
//从pos开始查找, 返回对应的字符/字符串在string中最开始的下标
//注意与find不同的是: find功能相似与strstr(), 此函数功能则与strpbrk()相似
//(四个重载版本对应不同的查找需要)
size_t find_first_of(const string& str, size_t pos = 0) const noexcept;
size_t find_first_of(const char* s, size_t pos = 0) const;
size_t find_first_of(const char* s, size_t pos, size_t n) const;
size_t find_first_of(char c, size_t pos = 0) const noexcept;
//从pos开始, 倒过来查找, 返回对应的字符/字符串在string中最后一次出现的下标
//注意与find不同的是: find功能相似与strstr(), 此函数功能则与strpbrk()相似
size_t find_last_of(const string& str, size_t pos = npos) const noexcept;
size_t find_last_of(const char* s, size_t pos = npos) const;
size_t find_last_of(const char* s, size_t pos, size_t n) const;
size_t find_last_of(char c, size_t pos = npos) const noexcept;
//从pos开始查找, 返回第一个与字符/字符串中所有字符都不匹配的字符的位置
//本函数的功能类似于strpbrk, 但与之相反
size_t find_first_not_of(const string& str, size_t pos = 0) const noexcept;
size_t find_first_not_of(const char* s, size_t pos = 0) const;
size_t find_first_not_of(const char* s, size_t pos, size_t n) const;
size_t find_first_not_of(char c, size_t pos = 0) const noexcept;
//从pos开始, 倒过来查找, 返回第一个与字符/字符串中所有字符都不匹配的字符的位置
//本函数的功能类似于strpbrk, 但与之相反
size_t find_last_not_of(const string& str, size_t pos = npos) const noexcept;
size_t find_last_not_of(const char* s, size_t pos = npos) const;
size_t find_last_not_of(const char* s, size_t pos, size_t n) const;
size_t find_last_not_of(char c, size_t pos = npos) const noexcept;
//返回pos开始的n个字符组成的子字符串(sub就是子部分的意思)
//注意返回的是新构造的string对象
string substr(int pos = 0,int n = npos) const;
//比较字符串, 返回值的情况与strcmp()函数相同
int compare(const string& str) const noexcept;
int compare(size_t pos, size_t len, const string& str) const;
int compare(size_t pos, size_t len, const string& str,
size_t subpos, size_t sublen = npos) const;
int compare(const char* s) const;
int compare(size_t pos, size_t len, const char* s) const;
int compare(size_t pos, size_t len, const char* s, size_t n) const;
//----------------------------------------------
//Non-member function overloads 非成员函数重载系列:
string operator+ (const string& lhs, const string& rhs);
string operator+ (const string& lhs, const char* rhs);
string operator+ (const char* lhs, const string& rhs);
string operator+ (const string& lhs, char rhs);
string operator+ (char lhs, const string& rhs);
bool operator== (const string& lhs, const string& rhs);
bool operator== (const char* lhs, const string& rhs);
bool operator== (const string& lhs, const char* rhs);
bool operator!= (const string& lhs, const string& rhs);
bool operator!= (const char* lhs, const string& rhs);
bool operator!= (const string& lhs, const char* rhs);
bool operator< (const string& lhs, const string& rhs);
bool operator< (const char* lhs, const string& rhs);
bool operator< (const string& lhs, const char* rhs);
bool operator<= (const string& lhs, const string& rhs);
bool operator<= (const char* lhs, const string& rhs);
bool operator<= (const string& lhs, const char* rhs);
bool operator> (const string& lhs, const string& rhs);
bool operator> (const char* lhs, const string& rhs);
bool operator> (const string& lhs, const char* rhs);
bool operator>= (const string& lhs, const string& rhs);
bool operator>= (const char* lhs, const string& rhs);
bool operator>= (const string& lhs, const char* rhs);
//交换两string, 此为泛型算法重载版本, 效率比成员函数高......
void swap(string& x, string& y);
istream& operator>> (istream& is, string& str);
ostream& operator<< (ostream& os, const string& str);
istream& getline(istream& is, string& str, char delim);
istream& getline(istream& is, string& str);
string::size_type类型:
与size_t类似(size_t是全局定义), size_type类型是string与vector模板中特有的类型, 通常用来表明string&vector的长度, 标准库中将size_type定义为unsigned类型, 但在不同的机器上, size_type的长度可以是不同的(但足够表示任何string对象的大小)
为了达到更好的程序移植性, 在使用vector&string时, 最好在需要的地方使用size_type, 而不用int或unsigned来替代
可以使用auto和decltype推断size_type类型
size_type的定义方法:
vector<int>::size_type count;
string<int>::size_type count;
string对象与字符串字面量相加
编译器会将字符串字面量转化为string类对象, 而后在根据string内部的operator重载规则来计算两个string的相加
注意普通的字符串字面量相当于C string, 所以两个字符串字面量相加是错误的
C++ string类与原生C string的区别:
速度 | 操作性 | |
---|---|---|
C string | 程序执行速度更快 | 操作更精细, 但稍显复杂, 容易出错(如越界赋值) |
C++ string | 稍慢 | 有大量成员函数, 操作更方便, 适合大型程序, 且更不容易出错 |
3.2.3: C++11范围for循环
与标准for循环相比, C++11引入的范围for循环在代码上更为简洁明了, 同时又能保证防止越界访问, 遍历序列使优先采用, 但如果只是部分访问, 则使用标准for更好
用于遍历容器和其他序列中的所有元素, 注意这里一个for只能遍历单维度的序列
前排提醒: 范围
//基本语法
for(declaration : expression){
statement
}
- declaration定义一个变量, 序列中的每一个元素都会转化成该变量的类型并赋给该变量, 最简单的方法是使用auto让计算机自己每次迭代都会重新赋值(暂且不知道是不是重新创建, CPU指令中的汇编看不懂)循环控制变量declaration, 并将其初始化为序列中的下一个值
例如: - expression表示的必须是一个序列, 比如匿名数组数组,vector或string等能够返回迭代器begin和end的类型的对象
注意的是, declaration只是中间变量, 无法修改原expression中的值, 如需修改则要使用指针或引用
//输出数组a的所有元素
int a[]={1,3,2,5,6};
for( int i : a){
cout<<i<<endl;
}
//将数组a的所有元素*2
for(auto& elem : a){
elem*=2;
}
警告: 使用范围for时不应该在过程中改变expression的大小, 即不应该在过程中添加或删除expression序列中的元素, 因为范围for在开始时预存了end()的值, 所以当增删元素时, 会导致原先的end()失效
3.2.3: 关于迭代器与下标
对于C++ STL容器, 最好使用迭代器访问, 使用下标容易出现一些问题(有可能越界), 所以下标最好只用于访问原生C对象
3.3.0: vector容器
vector特性:
vector是C++标准模板库中的部分内容,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。vector之所以被认为是一个容器,是因为它能够像容器一样存放各种类型的对象,简单地说,vector是一个能够存放任意类型的动态数组,能够增加和压缩数据。
可以将vector理解为一个可变大小的数组, 支持快速的随机访问, 并且能够在尾部快速插入元素, 但是正如普通数组一样, 在尾部之外的地方(如中间)插入元素的速度可能很慢!
相当于是对链表的替换, 直接使用类的成员函数来实现功能, 而不用自己来写相关的操作 ( 加快建构速度 )
通常情况下, vector是最优先考虑的选择, 因为vector有着相当好的泛用性, 除非有更好的理由选择其他顺序容器
vector的使用:
- 构造与析构
通常, vector的初始化基本不设定其大小, 即创建一个空的vector, 因为vector可以非常高效的延长, 而预设vector的长度可能使性能更差, 除非是需要设置n个元素值都一样
//是时候献上真正的函数声明了:
//默认初始化:
explicit vector (const allocator_type& alloc = allocator_type());
//注意前头的explicit
//填充初始化(就是画图的填充):
explicit vector (size_type n);
vector (size_type n, const value_type& val,
const allocator_type& alloc = allocator_type());
//范围初始化(部分拷贝):
template <class InputIterator>
vector (InputIterator first, InputIterator last,
const allocator_type& alloc = allocator_type());
//拷贝初始化:
vector (const vector& x);
vector (const vector& x, const allocator_type& alloc);
//这3个TM是啥.....
vector (vector&& x);
vector (vector&& x, const allocator_type& alloc);
initializer list (6)
vector (initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());
/******以下为旧的构造函数解释, Low了点*******************************/
vector<Type> VectorName
// 创建一个空的vector, 类型为Type, 标识符为VectorName
vector <Type>c1(c2) // 复制一个vector, 将c2复制给c1
vector <Type>c(n) // 创建一个vector,含有n个数据,数据均已缺省构造产生
//[会自动赋给一个初值, 但是根据数据类型进行的, 所以不一定是0]
vector <Type>c(n, elem) // 创建一个含有n个elem拷贝的vector
int a[]={1,2,3,4,5,6};
vector<Type> v5(a,a+4); //将数组a到a+4赋给v5
vector<int>v3(10,2); //创建一个含义10个int, 且值都为2的vector
//也可以用 大括号{} 进行列表初始化 :
vector<int> v4={10,2}; //v4有两个int元素, 分别为10, 2;
//和数组的初始化方法一样 [注意与圆括号()创建法不同!]
//析构函数
vectorName.~vector () // 销毁所有数据,释放内存
成员函数:
//-----------------------
//iterator迭代器系列函数:
//返回首迭代器
iterator begin() noexcept;
const_iterator begin() const noexcept;
//返回尾迭代器
iterator end() noexcept;
const_iterator end() const noexcept;
//-------------------------------------------------------------
//Capacity容量系列函数, 用于返回和设置容量
//返回容器中实际数据的个数。
size_type size() const noexcept;
//返回容器可承载的最大的元素数量, 包括可重新分配内存而达到的最大值
size_type max_size() const noexcept;
//返回当前申请的内存大小(以字节为单位), 由当前最大可容纳的元素个数觉得
size_type capacity() const noexcept;
//设置容器的当前元素数量为n个
//如果n大于当前元素数量, 则将新增的元素初始化成val, 如果未提供val, 则使用默认的构造函数
//如果n小于当前元素数量, 则删除多余的元素
void resize(size_type n);
void resize(size_type n, const value_type& val);
//设置容器的大小至少可容纳n个元素
//如果n大于当前capacity, 则扩容到n
//如果n小于当前capacity, 啥也不干
void reserve(size_type n);
//判断容器是否为空
bool empty() const noexcept;
//-----------------------------------------------------------------------
//Element access元素访问系列函数:
//类似于C原生数组的下标访问:
reference operator[] (size_type n);
const_reference operator[] (size_type n) const;
//传回索引idx位置的元素的引用, 越界会抛出out_of_range
reference at(size_type n);
const_reference at(size_type n) const;
//传回最后一个元素的引用(注意不检测是否初始化)
reference back();
const_reference back() const;
//返回第一个元素的引用
reference front();
const_reference front() const;
//返回一个指向vector首元素的直接内存指针(可用于C&C++混合编程)
//由于vector的元素是连续分配内存的, 所以可以通过指针偏移来访问元素
value_type* data() noexcept;
const value_type* data() const noexcept;
//----------------------------------------
//Modifiers编辑器系列函数, 用于修改元素
//拷贝赋值, 与拷贝构造函数相似......吧
void assign(InputIterator first, InputIterator last);
void assign(size_type n, const value_type& val);
void assign(initializer_list<value_type> il); //可变参数
//向容器的末尾压入一个元素
//如果这导致当前的元素数量超过capacity, 则会导致储存空间重新分配
void push_back(const value_type& val);
void push_back(value_type&& val);
//弹出最后一个元素, 使size-1
void pop_back();
//在pos位置插入一个或n个新元素, 并初始化为val, 导致size+1
//返回指向插入元素中第一个元素的迭代器
//如果这导致当前的元素数量超过capacity, 则会导致储存空间重新分配
//注意: 插入会导致pos后的元素被重新分配位置, 这是一个低效的操作
iterator insert(const_iterator position, const value_type& val);
iterator insert(const_iterator position, size_type n, const value_type& val);
template <class InputIterator>
//在pos位置插入从first到last的元素, 包括first, 但不包括last
iterator insert(const_iterator position, InputIterator first, InputIterator last);
iterator insert(const_iterator position, value_type&& val);
//不定参数版本
iterator insert(const_iterator position, initializer_list<value_type> il);
//删除制定的元素(直接删除元素释放内存, 而不仅仅是擦除数据), 导致size-1
//返回被删除元素的后一个元素的迭代器, 如果删除了最后一个元素, 则返回尾迭代器
//注意: 删除会导致pos后的元素被重新分配位置, 这是一个低效的操作
iterator erase(const_iterator position);
iterator erase(const_iterator first, const_iterator last);
//交换当前容器与相同类型的容器x中的元素
//注意: 此函数不会导致迭代器,引用和指针失效
void swap (vector& x);
//清除当前容器中的所有元素, 并将size置零
//注意: 使用此函数后储存空间可能会被重新分配
//但是如果需要强制重新分配, 通常使用vector<T>().swap(x);
void clear() noexcept;
//构造并在pos位置插入一个元素, 返回指向插入的新元素的迭代器
//功能上与insert类似, 但其避免了临时变量的产生
//insert采用的是将所提供的参数构造一个临时变量, 而后将其拷贝到pos位置
//emplace直接在pos位置用对应类型的构造函数构造一个元素, 所以即使构造函数为explicit也可以使用
template <class... Args>
iterator emplace(const_iterator position, Args&&... args);
//与push_back类似
template <class... Args>
void emplace_back(Args&&... args);
//----------------------------------------------------------------------
//allocator空间配置器函数
//返回当前容器的空间配置器(这个还没学到...先等等)
allocator_type get_allocator() const noexcept;
//---------------------------------------------------------
//Non-member function overloads 非成员函数重载
//各个关系运算符的重载
(1)
template <class T, class Alloc>
bool operator== (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
(2)
template <class T, class Alloc>
bool operator!= (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
(3)
template <class T, class Alloc>
bool operator< (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
(4)
template <class T, class Alloc>
bool operator<= (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
(5)
template <class T, class Alloc>
bool operator> (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
(6)
template <class T, class Alloc>
bool operator>= (const vector<T,Alloc>& lhs, const vector<T,Alloc>& rhs);
//交换容器x与y的所有元素, 功能与成员函数swap相似
//此为泛型算法的重载版本, 效率较高(比成员函数swap高)
template <class T, class Alloc>
void swap(vector<T,Alloc>& x, vector<T,Alloc>& y);
使用注意项:
- 赋值&添加:
采用类似数组下标的方式对vector进行访问, 下标访问的元素必须是已经存在的元素, 所以不可以用下标法进行元素的添加, 只能用来修改以存在的元素 - 访问:
可以使用下标法&STL通用迭代器进行元素的访问, 但通常使用迭代器, 这是作为C++程序员的习惯, 因为迭代器是STL容器中通用的, 但下标法只是vector的特性
3.4: C++ STL 迭代器
- 警告:
在STL容器中,string,vector与deque提供了随机访问迭代器(即可以进行加减数运算),list、set、multiset、map、multimap提供了双向迭代器(只能++ --的单向移动)
迭代器简介:
STL库的大部分容器都有其迭代器, 通过name.begin() & name.end()可得对应的首尾迭代器
可将迭代器理解为智能指针, 与指针的区别就是不能比较大小, 只能用= 和 != 来判断, 特别是在for中作为break条件时需要格外注意, 其他的使用和指针一样
合法的迭代器只能指向容器中的元素和最后一个元素的后一个空位子(即end()指向的位置), 其他的指向都是未定义的
迭代器变量的声明:
如:
//声明一个set的迭代器
set<int>::iterator it;
- 特殊迭代器类型:
const_iterator, 相当于指向const类型对象的指针, 只可使用此迭代器访问元素而不可修改此元素的值, 而const iterator相当于const类型的指针, 声明时必须初始化, 且之后不可再修改其指向的位置. 如果容器中的对象是一个常量, 则只能使用const_iterator
迭代器变量的使用:
*iter //对iter进行解引用,返回迭代器iter指向的元素的引用
iter->men //对iter进行解引用,获取指定元素中名为men的成员。等效于(*iter).men
++iter iter+ //给iter加1,使其指向容器的下一个元素
--iter iter-- //给iter减1,使其指向容器的前一个元素
iter1==iter2 iter1!=iter2
//比较两个迭代器是否相等,当它们指向同一个容器的同一个元素或者
//都指向同同一个容器的超出末端的下一个位置时,它们相等
//只有vector和queue容器提供迭代器算数运算(即加减乘除)和除!=和==之外的关系运算:
iter+n iter-n
//在迭代器上加(减)整数n,将产生指向容器中钱前面(后面)第n个元素的迭代器。
iter1+=iter2 iter1-=iter2
//将iter1加上或减去iter2的运算结果赋给iter1。
iter1-iter2
//两个迭代器的减法,得出两个迭代器的距离(特有的difference_type类型)
>,>=,<,<= //元素靠后的迭代器大于靠前的迭代器。
- 拓展
difference_type类型类似size_type类型, 用来表示两个迭代器之间的 距离 , 比如两个迭代器相减得到的数据类型就是difference_type, 他是一个有符号整型数, 具体为:
迭代器的失效:
注意: 为了防止迭代器失效,凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素
向容器中添加或删除元素都有可能会是迭代器失效, 即失效后的迭代器不在指向任何元素(类似野指针).
对于vector & string : 添加元素后(如用push_back()), 如果导致储存空间被重新分配, 则原先指向容器的所有迭代器都会失效; 否则只是插入位置后的元素的迭代器会失效;删除元素之后的迭代器都会失效;
对于deque: 向任何位置插入&添加元素都会导致迭代器全面失效
对于list & forward_list: 不论怎么插&删, 迭代器都是有效的
3.5.2: 下标访问元素
最好使用size_t类型的对象作为下标(之前使用的一直是int类型)
size_t 类型定义在cstddef & stddef.h头文件中, 表示C中任何对象所能达到的最大长度, 具体大小与机器相关,它是无符号整数
在原生的C数组中, 对下标的要求比较松, 可以是有符号整型, 即可以为负数, 此时访问的位置在首地址之前(虽然这种访问通常越界, 没意义). 而标准库类型中, 要求下标必须是无符号整型, 不能为负数
3.5.3: 标准库函数begin()&end()
定义在 interator 头文件中, 但使用时只需要 #include < iostream >即可
标准库函数begin()&end()功能与STL库中的成员函数类似, 后者返回首尾迭代器, 前者可用于原生C数组, 返回其首尾的指针(注意尾指针与末尾迭代器相似, 指向的是最后一个元素的下一个位置)
这两个函数可以减轻计算数组首尾元素指针的负担
int a[]={0,1,2,3,4,5,6,7,8,9};
printf("%p\n",begin(a));
printf("%p\n",end(a));
printf("%p\n",&(a[0]));
printf("%p\n",&(a[9]));
printf("%d\n",a[9]);
输出结果:
000000000071fdd0
000000000071fdf8
000000000071fdd0
000000000071fdf4
9
3.5.3: ptrdiff_t类型
与difference_type相似, ptrdiff_t是两个指针相减后的数据类型, 为有符号整型 , 定义在cstddef头文件中
3.5.5: C++工程代码优化
对于以C++为主要语言构建的工程代码, 应该避免使用原生C数组与指针, 而采用vector与string和对应的迭代器替代, 因为前者常用与底层操作, 容易因为疏忽而引发一些细节错误, 对于面向对象的编程不利
Extra拓展知识: 原生字符串(RawString)
- 原始字符串中的字符表示的就是自己, 例如:
cout<<R"(hello,"Bob".)"<<endl;
输出效果:
hello,“Bob”.
- 原始字符串还可以自定义定界符
默认定界符是"(和)"。因此若想要在字符串中允许)",则必须自定义定界符
自定义定界符的方法就是在"和(之间添加字符,当然在末尾的定界符应保持一致。以上例子自定义的定界符是"+(,则末尾定界符是)+"。自定义定界符时,在默认定界符之间添加任意数量的基本字符,但空格,左括号,右括号,斜杠和控制字符等除外。
例如:
cout<<R"+*("(Who is it?)")+*"<<endl;
输出效果:
“(Who is it?)”
Chapter IV
4.0.0: Chapter IV的核心
4.1.1: 简单的左右值判断
当一个对象被用作右值的时候,用的是对象的值(内容)
当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
4.5.0: 关于++i & i++
除非必须, 否则正常情况下不使用++和–的后置版本, 因为:
- 前置版本避免了不必要的工作, 他把值+1以后直接返回改变了运算对象
- 而后置版本中, 需要将原始的值储存下来以便返回这个为修改的内容, 如果这个内容没用, 则形成了一种浪费
4.5.0: 简洁的编码习惯
如:
cout << *iter++ << endl;
cout << *iter << endl;
iter++;
两者是等价的, 但更推荐第一种写法(这种看似难以理解的写法其实在实际的编码中更为常见, 被广泛使用, 书中这么说…), 对于此类被大众广泛认可的以牺牲理解度而换来的简洁, 最好是接收, 并且融入到实际应用中
但是注意, 太过反人类的写法不是什么小聪明
抬手就是一波看不懂的操作绝不会让你成为公司里无可替代的人
代码本身最好保证其有良好的可读性, 即便繁琐一点也不失优雅;
4.8.0: 关于位运算符
位运算符的运算对象可以是有符号的,也可以是无符号的, 但是对于有符号的类型,符号位如何处理没有明确的规定, 如移位运算符<<和>>, 对于有符号类型, 使用右移运算符时, 可能在最左端插入符号位的副本, 也可能插入0, 具体视环境而定
所以强烈建议仅将位运算符用于处理无符号类型
4.9.0: 关于sizeof
sizeof不会实际求运算对象的值, 所以有:
int *p;
sizeof *p; //合法, 编译通过
即便*p是一个未初始化的野指针, 对其sizeof也没有什么影响, 因为p并没有被真正解引用, 最后会正常返回p指向的数据类型的大小
还有需要注意的是:
- 对string或vector对象执行sizeof只会返回该类型固定部分的大小, 不会计算对象中的元素实际占了多少空间
- 对指针使用sizeof会返回指针的大小, 而解引用的指针会返回指向的数据类型的大小, 而对于数组首地址(即数组名)使用sizeof则会返回整个数组的大小, 这一点编译器会与指针区分开
4.10.0:逗号运算符
注意逗号运算符返回的是逗号右边的表达式的值, 而将其左边的表达式求值后丢弃, 如果右侧运算对象是左值,那么最终的求值结果也是左值
4.113: 显示(强制)类型转换
关于强制类型转换, C语言中的转换可以实现C++的4种转换的功能
C语言中的显示类型转换:
int i=9,j=10;
double temp=(double)j/i;
而C++中的强制类型转换形式如下:
cast-name<type>(expression)
其中C++提供了4种不同的cast-name类型:
-
static_cast
任何具有明确地域的类型转换,只要不包含底层const, 都可以使用static_cast, 如:double slope = static_cast<double>(j)/i;
使用static_cast可以明确的告诉编译器和读代码的人, 忽略转换过程中的潜在的信息损失
-
dynamic_cast
-
const_cast
const_cast只能改变运算对象的底层const, 通常用于变更对象的const属性(附上或消去)
- 对于附上const属性, 没啥好讲的
- 而对于消去const属性, 如:
const int constant = 21;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 7;
编译器不会报错, 但是 这是一个未定义行为 , 具体如何处理会由编译器决定, 如将结果输出:
constant: 0x7fff5fbff72c
const_p: 0x7fff5fbff72c
modifier: 0x7fff5fbff72c
constant: 21
const_p: 7
modifier: 7
可以看到虽然三个指针指向的是同一个地址, 但最终输出的值并不相同, 所以无论如何要避免这种写法
cosnt_cast常常用于有函数重载的上下文中, 还有一种特殊需求可以使用const_cast:
- 定义了一个非const的变量,但之前用带const限定的指针去指向它,在程序后段的某一处突然又想修改了,这时候可以消去const来修改
但终归这种需求是可以通过其他方式实现的, 所以总体上应避免这种用法
-
reinterpret_cast
reinterpret_cast通常为运算对象的为模式提供了较低层次上的重新解释。使用方法:reinpreter_cast (expression)
说明:type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值), 如:
int *ip; char *pc = reinterpret_cast<char*>(ip);
reinterpret_cast可以转换任意一个32bit整数,包括所有的指针和整数。可以把任何整数转成指针,也可以把任何指针转成整数,以及把指针转化为任意类型的指针,威力最为强大!但不能将非32bit的实例转成指针。总之,只要是32bit的东东,怎么转都行!
因为任何指针可以被转换到void*,而void*可以被向后转换到任何指针(对于static_cast<> 和 reinterpret_cast<>转换都可以这样做),如果没有小心处理的话错误可能发生
所以对于reinterpret_cast的使用, 必须对涉及的类型和编译器实现转换的过程都非常了解, 才能保证最基本的安全
总体而言, 最好的建议是尽量避免使用强制类型转换, 每次写了一条强制类型转换语句,都应反复斟酌: 是否能以其他方式实现相同的目标。就算实在无法避免, 也应该尽量限制类型转换制的作用域, 并且记录对相关类型的所有假定, 以减少错误发生的机会
Chapter V
5.1.0: 空语句
最常用的空语句就是在while中:
while(cin>>s&&s != sought)
; //空语句
使用空语句时最好加个注释, 从而令阅读者知道该语句是有意而为之
5.3.2: 关于switch
- 最好在switch中的最后一个case后加上break, 尽管这样没什么差别, 但是当向末尾添加其他的case时, 防止了漏补break的Bug
- 最好在switch的最后添加一个default, 即便什么也不做. 其目的在于告诉阅读者已经考虑到了所有case以外的默认情况
- 一个容易Error的问题:
当在case所掌控的代码段中定义变量, 最好将变量定义在case独立掌控的块(block)中, 否则当switch想跳过某个case时, 无法绕过带有初始化的变量的定义而直接跳转到该变量作用域内的另一个位置:
case true:
string file_name; //非法: 控制流试图绕过一个隐式初始化变量
int ival=0; //非法: 控制流试图绕过一个显示初始化变量
int jval; //合法: jval没有被初始化
case false:
jval = num;
当程序跳转到false时, 试图绕过ival和file_name的定义, 但是false的部分仍在ival和file_name的作用域中, 如果成功跳过, 程序将在后头使用没有初始化的ival和file_name, 这是非法的
所以最好将变量定义在块(block)内, 以确保后头的程序都在变量的作用域之外
5.5.3: 关于goto
再次提醒, 不要在工程代码中使用goto, 这是素质吊差的行为, 使代码的可读性下降且难以修改
通常想要使用goto功能的地方都可以通过其他方法实现, goto 最多只能运用在竞赛代码的临时修补上(因为反正后头就没用了)
5.6.0: 异常处理
Chapter V的重点部分
基础部分:
程序运行时常会碰到一些异常情况,例如:
做除法的时候除数为 0;
用户输入年龄时输入了一个负数;
用 new 运算符动态分配空间时,空间不够导致无法分配;
访问数组元素时,下标越界;打开文件读取时,文件不存在。
这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃。
所谓“处理”,可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行;也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存中的数据写入文件、关闭打开的文件、释放动态分配的内存空间等。
C++ 引入了异常处理机制, 采用try & throw & catch, 其基本思想是:
try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出, 如果期间没有发生异常, 则在try块全部执行完后执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。
程序在遇到异常时用throw抛出, 而后程序终止下面代码的执行, 开始寻找异常处理代码。
寻找处理代码的过程与函数的调用链刚好相反。假定函数A遇到异常并抛出, 程序首先会在函数A内部寻找匹配的catch字句, 即对应的处理代码, 如果没有找到,则立刻return函数A, 并开始在函数A的调用者函数B中寻找对应的处理代码, 以此一层层的上推, 直到最外层的main函数.
main 函数应该处理异常, 如果main函数也不处理异常,称为异常被顶层抛出, 系统会自动调用默认的处理函数unexpected来执行(程序员可以自定义unexpected函数来替代默认的选择), 导致程序立即异常地中止, 表现为出现一个崩溃对话框。
具体的使用语法:
try {
语句组
throw 异常类型;
throw int(12); //抛出一个int异常类型的值为12的异常(其中调用int构造函数)
}
catch(int err) { //捕获一个int类型的异常并将值赋给err
//(这里类似函数的参数, 也可以使用引用, 但是指针被判定为另一种类型)
异常处理代码
}
...
catch(异常类型) {
异常处理代码
}
//catch 可以有多个,但至少要有一个
能捕获所有类型异常的catch
如果希望不论拋出哪种类型的异常都能捕获,可以编写如下 catch 块:
catch (double d) {
cout << "catch (double)" << d << endl;
}
catch (...) { //中间三个点, 捕获当前未捕获的所有类型的异常, 相当于default
cout << "catch (...)" << endl;
}
异常的传递抛出
如上头所言, 如果一个函数在执行过程中拋出的异常在本函数内就被 catch 块捕获并处理,那么该异常就不会拋给这个函数的调用者(也称为“上一层的函数”);如果异常在本函数中没有被处理,则它就会被拋给上一层的函数, 如:
#include <iostream>
#include <string>
using namespace std;
class CException
{
public:
string msg;
CException(string s) : msg(s) {}
};
double Devide(double x, double y)
{
if (y == 0)
throw CException("devided by zero");
cout << "in Devide" << endl;
return x / y;
}
int CountTax(int salary)
{
try {
if (salary < 0)
throw - 1;
cout << "counting tax" << endl;
}
catch (int) {
cout << "salary < 0" << endl;
}
cout << "tax counted" << endl;
return salary * 0.15;
}
int main()
{
double f = 1.2;
try {
CountTax(-1);
f = Devide(3, 0);
cout << "end of try block" << endl;
}
catch (CException e) {
cout << e.msg << endl;
}
cout << "f = " << f << endl;
cout << "finished" << endl;
return 0;
}
程序的输出结果如下:
salary < 0
tax counted
devided by zero
f=1.2
finished
使用异常处理的好处:
虽然函数也可以通过返回值或者传引用的参数通知调用者发生了异常,但采用这种方式的话,每次调用函数时都要判断是否发生了异常,这在函数被多处调用时比较麻烦。有了异常处理机制,可以将多处函数调用都写在一个 try 块中,任何一处调用发生异常都会被匹配的 catch 块捕获并处理,也就不需要每次调用后都判断是否发生了异常。
异常内部处理后再传递抛出
有时,虽然在函数中对异常进行了处理,但是还是希望能够通知调用者,以便让调用者知道发生了异常,从而可以作进一步的处理
#include <iostream>
#include <string>
using namespace std;
int CountTax(int salary)
{
try {
if( salary < 0 )
throw string("zero salary");
cout << "counting tax" << endl;
}
catch (string s ) {
cout << "CountTax error : " << s << endl;
throw; //继续抛出捕获的异常
}
cout << "tax counted" << endl;
return salary * 0.15;
}
int main()
{
double f = 1.2;
try {
CountTax(-1);
cout << "end of try block" << endl;
}
catch(string s) {
cout << s << endl;
}
cout << "finished" << endl;
return 0;
}
程序的输出结果如下:
CountTax error:zero salary
zero salary
finished
函数的异常声明列表(异常规格说明)
为了增强程序的可读性和可维护性,使程序员在使用一个函数时就能看出这个函数可能会拋出哪些异常,C++ 允许在函数声明或定义时,加上它所能拋出的异常的列表,但是如果在函数的定义和声明中都写了异常声明列表, 则需要保证两者一致, 具体写法如下:
void func() throw (int, double, A, B, C);
void func() throw (int, double, A, B, C){...}
// 表明func可能抛出int, double, A,b,c 五中异常
void func()v throw();
//表明函数不会抛出任何异常
注意: 如果一个函数不交代会抛出那些异常, 则其可能抛出任何异常
如果函数抛出了异常声明列表中没有的异常, 在编译时不会有任何问题, 但在运行时可能会崩溃, 或者是异常声明列表不起实际作用
异常规格说明的意义(增强函数的可读性和可维护性)\
- 其实函数的调用者函数可能会抛出哪些异常, 并做好异常处理的准备
- 提示函数的维护者不要抛出出这些异常外的其他异常
- 异常规格说明是函数接口的一部分\
C++标准异常
C++标准库中定义了一组标准异常, 用于报告标准函数库函数遇到的异常, 同样, 使用者也可以自定义的使用这些异常, 他们分别被定义在4个头文件中:
- exception 头文件定义了最通用的几个异常类, 其只报告异常的发生, 不提供任何额外的信息
- stdexcept 头文件定义了几种常用了异常类:
//<stdexcept>定义的异常类, 注意其中每个异常都是一个类
exception //最常见的问题
runtime_error //只能在运行过程中才能检测出的问题, 即运行错误
range_error //运行错误: 生成的结果超出了有意义的值域范围
overflow_error //运行错误: 计算上溢
underflow_error //运行错误: 计算下溢
logic_error //程序逻辑错误
domain_error //逻辑错误: 参数对应的结果值不存在
invalid_argument //逻辑错误: 无效参数
length_error //逻辑错误: 试图创建一个超出该类型最大长度的对象
out_of_range //逻辑错误: 使用一个超出有效范围的值
- type_info 头文件定义了bad_case异常类型
以上几种类型都会在后头有较详细的说明
其中, 每种异常类中都定义了what成员函数, 返回C原生字符串形式的异常描述信息, 并且还定义了几种基本的运算, 包括创建或拷贝异常类型的对象, 以及为异常类型的对象赋值
在使用时, 只允许程序员以默认初始化的方式初始化exception, bad_alloc, bad_cast对象, 而不允许对这些对象提供初始值, 而对于其他异常类型, 应该使用string对象或原生C字符串初始化这些类的对象, 不允许使用默认初始化的方式, 其中字符串中包含的是与发生的错误相关的信息, 发生错误后使用what会返回定义的初始值, 而对于前三种默认初始化的异常类, 返回的内容由编译器决定
Chapter VI
6.1.2: 关于函数声明问题
再次强调, 函数声明时最好带上变量名, 而不是只有一个变量类型, 因为函数在调用时显示的提示是函数的声明, 且阅读者通常看到的也只是函数的声明. 其定义通常对调用者隐藏, 所以在函数的声明中保留变量名可以提醒使用者变量的意义, 以便更好的理解函数的功能
在多文件的工程中, 最好将函数的声明放在头文件中, 而将函数的定义放在源文件中, 同时在源文件中include函数定义的头文件, 这样可以利用编译器来检验函数的声明和定义是否正确
6.2.2: 关于函数的引用传递
在定义函数时, 如果不需要在函数内部修改参数的值, 最好使用常量引用传递(即const &), 防止发生不必要的修改
若非必要, 不要使用拷贝传递(效率低下)
[回顾: ]关于引用与指针
指针不能指向引用, 因为引用本身不是对象, 但是可通过对引用取地址&, 得到引用绑定的对象的地址
int &*p; //*与p更接近, 所以p表示的是一个指针, 其指向的是int&, 所以Error
int *&p=nulptr; //&与p更接近, 所以p表示的是一个引用, 其引用的是int*
int &temp=rowTime;
printf("%p",&temp); //打印的是rowTime的地址
6.2.6: C++中可变形参的函数
在C语言中, 可采用< stdarg.h >中的va_list来整变参函数, 类似printf的实现
而在C++中, 用initializer_list关键字来处理参数数量可变但类型相同的情况
initializer_list的使用类似C++STL中的list, 除了initializer_list中的内容不能被修改以外, 其他的操作基本与list相同
-
初始化
initializer_list<T> listName; // initializer_list<T> listName{a,b,c, ...}; //
初始化时必须指定T的类型, 而后编译器会判断列表中的元素类型与T是否相同, 如果列表中的参数不一致, 则会给出warning
常规操作:
//拷贝操作, 二者等价 //但具体操作类似让list2指向list1的元素, 并不会真正的拷贝列表中的元素 list2(list1); list2=list1; //返回首尾指针, 与迭代器类似 list.begin(); list.end(); list.size(); //返回列表成员数量, 为size_t类型 //函数定义: size_t size() const noexcept; //C++11
具体应用:
void error_msg(int num, initializer_list<string> il, int fucker){
for(auto temp : il){
cout<<temp<<endl;
}
printf("Num= %d\n",num);
printf("Fucker= %d\n",fucker);
return ;
}
int main(int argc, char *argv[])
{
error_msg(250,{"WDNMD","NMSL","TQL wsl"},12345); //注意传参时要加个大括号
return 0;
}
程序输出:
WDNMD
NMSL
TQL wsl
Num= 250
Fucker= 12345
再次强调, 给函数传参时initializer_list部分要加个大括号, 表示其中的元素是属于一整个list的, 这样才可以在函数调用中再前后和后头添加其他的参数, 否则编译器就不知道到底有哪些元素是在list中的, 这种情况就类似于C语言中的va_list, 所以使用va_list的函数为何要将va_list放在最后一个参数, 并且还需要parameter number
这里复习一下C语言中的va_list:
使用方法: <必须严格按照规范>
-
提供一个带有省略号…的函数原型, 此函数必须至少存在一个形参, 且…必须是最后一个形参, 且函数最右边的形参(即…左边的形参)被称为paramN(英语全拼: parameter number ,即参数数量), 表述了…部分代表的参数数量.
如:void f1(int num, …); f1(3, 111, 222, 333); //num即为paramN, 调用时num=3, 表示…部分有3个参数
-
在函数定义中创建一个va_list类型的变量
va_list定义在stdarg.h中, va_list类型的变量用于储存…中的数据对象
如:va_list list; //此时创建的只是一个空对象
-
用宏把该变量初始化为一个参数列表
使用va_start()宏,//函数定义: void va_start (va_list ap, paramN); //第一个参数是需要初始化的va_list变量(即上头创建的) //第二个就是函数形参中的paramN
如:
va_start(list, num); //在初始化后va_list对象才有真正的意义
-
用宏访问参数列表
使用va_arg()宏://函数定义: type va_arg (va_list ap, type); //第一个形参为上头初始化完成的va_list变量 //第二个形参为va_arg()的返回类型 //即可以根据第二个参数决定将参数列表中的参数解析成什么类型
当第一次调用va_arg时, 它返回参数列表中的第一个, 再次调用时返回第二个, 以此类推
如:double num1=va_arg(list, double); int num2=va_arg(list, int);
如果此时的返回类型和传入的参数类型不一致的话可能会发生错误(其实就是数据的解释错误, 如将double解释为int)
-
用宏完成清理操作
使用va_end()宏清理上头用va_list()动态申请的内存函数定义: void va_end (va_list ap);
如:
va_end(list);
一旦使用va_end后, list并不会无效化, 其相当于指向用calloc申请的内存的指针, 只是申请的内存被释放了, 指针还是可以访问的, 但是需要用va_start重新初始化才能使访问有效;
具体例子:
void testFun(int num, ...){ va_list list; va_start(list, num); for(int i=0;i<num;++i){ printf("%f ",va_arg(list, double)); } printf("\nAll End\n"); va_end(list); return ; }
va_list与initializer_list的比较
二者的功能相似, 都用于参数数量可变但参数类型不变的函数
-
针对不同类型的参数
二者都可以重载来适应不同的参数类型, 但是在重载时, va_list真正需要变化的是函数内部的va_start部分, 函数头只是根据重载的需要作出刻意的改变, 而initializer_list的参数是必须改变的而不是刻意而为之, 所以在适应不同类型的方便性上, initializer_list更占优
-
针对方便性
在函数编写的方便性上, va_list的操作明显比initializer_list要复杂(有5步), 而initializer_list更能适应与C++的要求
所以在C++工程项目中, 如非必要, 优先使用initializer_list
6.3.2: 关于函数返回值
函数在有多条return语句时, 如果这些语句没有全覆盖(即存在使函数未经return就结束的可能), 编译器可能检测不出错误(但通常都没问题), 一旦检测不出, 且函数通过这种方式退出, 这种行为是未定义的
6.3.2: 函数返回引用或指针时
虽然这种情况不常见, 但是确实可行: 当函数返回参数的引用或指针时, 可以对其赋值:
如:
get_val(stringName, index)='A';
//get_val返回string对象stringName[index]的引用, 此时可以对其赋值
当且仅当函数返回的引用或指针有效时此种方法可行, 如果函数返回的是其块作用域内的变量的引用或指针, 此方法是未定义的
6.3.2: 关于递归
在C语言中, 允许main函数递归调用, 而C++中不允许
(C语言是相当宽松的, C++为了去除这一点, 通常要求的比C语言更加严格)
6.3.3: 返回数组指针的函数
注意是数组指针, 即指向数组的指针(二级指针以上), 不是普通的指针
- 直接定义:
int (*func(int i))[10];
//函数返回一个指向内含10个int的数组的指针
· func(int i)表示调用func函数时需要一个int类型的实参
·(*func(int i))意味着我们可以对函数调用的结果执行解引用操作
·(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
· int(*func(int i))[10]表示数组中的元素是int类型
注意: 函数的形参列表必须紧跟在函数的后头, 优先与数组的维度
- 使用using或typedef设置别名
这个方法没什么特点
typedef int arrT[10];
using arrT=int[10];
//二者是等价声明
arrT *func(int i);
//与第一种方法效果相同
- 使用尾置返回类型
C++11新特性
任何函数都可以使用尾置返回, 但是这种方式对于返回类型比较复杂的函数最为有效
auto func(int i) -> int(*)[10] ;
语法格式:
在原本的函数返回类型处放一个auto, 而将真正的返回类型丢到后头去, 并用一个->指向
这种方法适用于较为复杂的返回类型, 主要是提升可读性
对于返回类型很复杂的函数而言, 这个方法就没啥用了:
//返回指向数组的指针
auto func1(int arr[][3], int n) -> int(*)[3] {
return &arr[n];
}
//原版本:
int(*(*func2())(int arr[][3], int n))[3]{
return func1;
}
//使用新特性的版本
auto func2() -> int (*(*)(int arr[][3], int n))[3] {
return func1;
}
//函数接受一个指向func1函数的指针的参数,返回指向func2的函数的指针;
auto func3(int(*(*ptf)(int arr[][3], int n))[3]) -> int (*(*(*)())(int arr[][3], int n))[3]{
...
}
- 使用decltype
以上的复杂返回值情况可以使用decltype来解决:
//等效上头的fun3
decltype(fun2)* func3(decltype(fun1)* ptf); //简单多了
需要注意的是: decltype 和 auto都只是返回对象的类型, 不会自动转化为指针, 需要程序员手动加一个*
[拓展: ]关于四者的选用:
第一种直接声明的方法暂不讨论
第二种using&typedef 的方法基本是万能的
第三种方法推荐在类型不是很复杂的时候使用
第四种方法推荐在类型特别复杂时使用, 特别是类型为某个其他类型的嵌套
6.4.1: 关于函数重载的注意点
注意: main函数不能重载, 会产生编译错误
函数重载的适用场合:
用于那些功能性非常相似的函数, 重载减轻了起名字的负担, 但是重载不适合用于特殊操作的函数, 相同的名字会降低对函数功能的理解
[回顾:]关于顶层const & 底层const:
顶层const:
表示任意对象本身就是常量,对任何数据类型都适用,如指针常量。
底层const:
表示所指对象时一个常量,如指向常量的指针。
const int *ptr=nullptr; //底层const, const离int近, 修饰的是指向的对象
int* const ptr=nullptr; //顶层const, const离ptr近, 修饰的是指针本身
const离哪个进就修饰哪个, 这俩不要再搞混了
6.4.2: 函数重载和const
之前说到, 函数重载时形参有无顶层const修饰无法作为区分条件:
int lookup(int a);
int lookup(const int a);
//这两个函数在调用时无法区分, 因此不能成功重载
但是如果顶层const修饰的是参数的指针或引用, 则可以重载:
int lookup(int *a);
int lookup(const int *a);
//成功重载
int lookup(int &a);
cosnt int lookup(const int &a);
//成功重载
编译器通过判定调用时的参数是否是const量来区分两个函数
而对于int lookup(int &a), 如果内部没有特殊的操作的话, 可以直接使用lookup(const int &a):
int& lookup(int &a){
return const_cast<int&> (lookup(const_cast<const int&>(a)));
}
本方法除去了复制代码的冗余问题
但是注意的是: 在lookup函数内部使用上一个const版本的lookup时, 必须保证其已经被声明过了
6.4.1: 函数重载与作用域
函数重载是在同一个作用域中的, 即不同作用域中的函数无法相互重载
如以下程序: 4个重载函数两个声明在main外头, 两个声明在main内部
void testFun(int a);
void testFun(char a);
int main(void){
void testFun(double a);
void testFun(string a);
char a='a';
testFun (a);
return 0;
}
void testFun(int a){
printf("This is the int function.\n");
return ;
}
void testFun(double a){
printf("This is the double function.\n");
return ;
}
void testFun(char a){
printf("This is the char function.\n");
return ;
}
void testFun(string a){
printf("This is the string function.\n");
return ;
}
输出结果:
This is the double function.
即内部的函数掩盖了外部的同名函数, 编译器在最接近函数调用处的作用域内寻找函数, 如果找不到才一层一层的向外头的作用域寻找
此程序中再main作用域内部找到了double型的和string型的testFun, 编译器就停止了寻找, 开始在这两个函数中选择参数类型最匹配的使用, 而char最匹配的是double, 于是编译器就将a做了一个隐式强制类型转换变成了double传给testFun
6.5.1: 关于函数参数的缺省值
一个函数可以在同一个作用域中被声明多次, 但是关于函数的同一个参数只能被赋予一次缺省值:
//假设这4个函数都声明在同一个作用域内
string screen(int a,int b); //初始声明
string screen(int a,int b=0); //正确, 补充了b的缺省值
string screen(int a=0,int b); //正确, 补充了a的缺省值, 且后头的b已经有了缺省值
string screen(int a,int b=1); //错误, b在上头已经被赋了缺省值
string screen(int a,int b=0); //错误, 即使给b赋相同的缺省值也不行
且局部变量(块作用域变量)不可作为缺省值, 必须是全局变量(文件作用域变量), 且如果在函数调用前改变了这个全局变量的值, 在函数后续的调用中参数的缺省值也会被改变(即缺省值是在调用时才真正赋值的)
当函数调用处的作用域内存在同名的局部变量掩盖的某个全局变量, 而这个全局变量是此函数的缺省值, 则函数参数使用的缺省值还是那个全局变量, 局部变量对其没有影响
能够类型转化的表达式都可以作为缺省值
6.5.2: constexpr函数
之前说到, constexpr用于修饰可在编译期求值的表达式, 当其修饰函数时也必须如此, 即constexpr修饰的函数必须是:
- 函数内部有且只有一条return语句(constexpr)
- 函数返回值类型和所有的参数类型都必须是字面量
所有的constexpr函数都会被内联, 并在编译期被替换为结果值(constexpr的操作)
constexpr函数&内联函数都允许在程序中多次定义(普通函数只支持多次声明), 所以通常将他们的定义放在头文件中
关于constexpr函数的作用:
- 用于替代C语言中的宏函数(即用define定义的函数, 其本质上还是宏macro)
而关于宏函数的作用, 只能说是具有特定特征的函数被写成了宏函数用于提升程序运行的效率, 所以constexpr函数的作用也类似于此
6.5.3: 关于断言库assert
断言库依赖NDEBUG预处理变量的状态:
#define NDEBUG
//定义后即为non-debug非调试版本, 此后assert什么也不做
断言库在C++工程程序的DEBUG时期非常有用, 结合#ifdef #ifndef #endif 使用可以极大的方便DEBUG时期的程序监控
6.5.3: DEBUG辅助工具
可以利用编译器定义的局部静态变量:
//这几个都是
cout<<__func__<<endl; //const char静态数组, 储存这条语句所在的函数的名字
cout<<__FILE__<<end; //const char静态数组, 储存这条语句所在的文件名
cout<<__LINE__<<endl; //const int整型静态变量, 储存这条语句所在的代码行数
cout<<__TIME__<<endl; //const char静态数组, 储存文件编译时间
cout<<__DATE__<<endl; //const char静态数组, 储存文件编译日期
6.6.0: 函数重载的匹配
首先相信编译器会匹配一个最佳的函数重载版本, 如果编译器无法判断该选择哪一个重载版本时会报错
但是作为函数的编写者, 就需要仔细考虑什么参数会调用到什么函数, 从而更好的优化函数的内部实现:
- 确认候选函数:
与被调函数同名, 且在调用点可见(被覆盖的不算) - 确认可行函数:
形参数量与本次调用的实参数量相等, 且类型都相同, 或是能转换成对应的类型 - 寻找最佳匹配:
以尽可能少的类型转换和尽可能小的类型转换(其中涉及到强制类型转换的分级)为判定标准, 从可行函数列表筛选出最合适的, 如果此时还无法判断, 则会报告Error并停止编译
6.7: 函数指针
指向函数的指针
注意使用函数指针调用函时:
//二者等价:
pf=&fun1;
pf=fun1;
//三者等价:
pf(n1,n2); //函数指针可以不用解引用的使用
(*pf)(n1,n2);
fun1(n1,n2);
当使用函数指针指向重载函数时, 指针必须和对应函数的某一个重载版本严格匹配, 编译器会通过参数类型确定指向哪个函数
Extra: 脑洞: 关于多个重载版本ambiguous问题:
void testFun(void){
printf("Function 0\n");
return;
}
void testFun(int a=1){
printf("Function 1\n");
return;
}
void testFuc(int a=1,int b=2){
printf("Function 2\n");
return;
}
int main(void){
testFun(); //此时编译器报出ambiguous错误, 无法判定具体使用哪一个重载版本
//如果就TM想用第一个重载版本, 可以用函数指针强制锁定:
void (*pf)(void)=testFun;
(*pf)();
//注意: 用别名的函数类型指针会出问题:
typedef void (*FP) (void); //无论是typedef还是using定义的别名都不可用
using FP = void (*) (void);
FP fp0= nullptr; //成功定义
fp0=testFuc; //但是在指向时判定的testFun为第三个重载版本
return 0;
}
所以不要挖坑给自己跳啊
Chapter VII:
7.0.1: 预<书中没有>: 类的定义:
标准格式, 即类的抽象数据类型(未被实体化):
class 类名 {
public: //public类型的成员&成员函数与结构相同, 可以被外部访问
… ;
protected: //protected类型与private类似, 但是派生类对象的内部能够访问
… ;
private: //private类型是class的核心, 用于对定义的类进行严密的封装, 是外部无法访问
…
}; //与结构体相同, 都需要加分号
类的封装赋予 了C++面向对象的特性
类的设计者负责类的实现过程, 并且对其实现加以封装, 并留下外部接口, 一个设计良好的类, 既要有直观且易于使用的接口, 也要具备高效的实现过程
而对于类的使用者, 只需要抽象的思考类做了什么, 而不需要顾及类的实现机理
7.0.2: 在类中定义函数:
(这一点与结构有一些差别, C的结构中不允许定义函数, 但是C++的结构允许定义函数) 通常只在类的声明中声明函数头, 并不进行定义(防止内部实现泄漏), 而在类声明的外部进行定义, 如果函数很短小, 也可以声明为inline:
但是注意: 定义在类内部的函数自动inline(不推荐)
//类成员函数定义方法:
className::functionName(...){
...
}
//这里就是多了一个类名, 表示定义的是 namespace 类名中的函数, 而不是其他范围内的函数
注意: inline的成员函数应该与相应的类定义放在同一个头文件中
7.0.3: 类的初始化:
可以与结构一样使用初始化器(但最好还是顺序初始化), 但只有public类型的函数能用初始化器的方法初始化(否则只能使用public的函数) , 但是通常使用构造函数进行初始化
构造函数:
构造函数为public(因为需要被外部访问), 在声明相应的类型对象时会隐式调用构造函数, 但也可以根据需要进行显式调用
函数名为类名, 无返回值(这里的无返回值是声明时根本连返回的类型都没有(void也算一种返回类型), 如 student (…, …, …, …); 前头不需要说明返回类型), 可以重载, 声明&定义同普通的成员函数
析构函数:
用来在对象生存期结束后对其进行销毁, 声明方法为与普通成员函数相同, 类型为public, 函数名为~+类名, 无返回值&参数, 且一个类对象只有一个析构函数
析构函数会在类对象生存期结束后被隐式调用, 但也可以根据需要进行显式调用(通常是用动态内存的方法创建的类对象需要手动销毁, 否则一般很少手动调用析构函数), 调用后会先执行函数内部的代码, 全部执行完后再销毁类, 所以函数内部不需要整如何销毁函数, 只需要完成销毁之前需要完成的任务(提示, 重置等等);
如:
//声明:
class student{
public:
student(void);
student(string tempName);
...
~student();
private:
string name;
...
};
//定义:
student::student(string tempName){
this->name=tempName;
}
//调用:
int main(void){
student student0; //调用student::student(void); 构造函数, 注意没有括号
student student1("Lao Wang"); //调用student::student(string tempName); 构造函数
student1.~student (); //主动调用student::~student(); 析构函数
return 0; //系统自动调用student::~student(); 析构函数
}
[注意:] 关于类的默认构造函数
类在声明时系统会提供默认的构造函数&析构函数(称为合成的默认构造函数/析构函数), 其初始化类的数据成员的规则如下:
-
如果存在类内的初始值, 则用他来初始化相应的类成员
-
否则, 编译器按以下规则合成有用的默认构造函数(即会对相应成员进行初始化):
1. 含有包含默认构造函数的成员类对象
2. 一个类的父类有自建的无参构造函数(有non-trival的默认构造函数)
3. 一个类里隐式的含有Virtual tabel(Vtbl)或者pointer member(vptr),并且其基类无任何构造函数或者有用户自定义的默认构造函数
4. 如果一个类虚继承与其他类(这一个与第三种情况类似) -
如果不符合以上情况, 编译器合成一个除了申请内存以外啥也不干的构造函数, 即对象中的成员都没有被初始化
而合成的析构函数只负责释放类对象申请的内存, 注意只是对象本身占用的内存, 如果类中有指针成员, 且指向采用动态内存方式申请的内存, 则这部分内存不会被释放, 此时需要自建析构函数, 否则将造成内存泄漏
如果需要更多的功能, 则需要自定义这两个函数, 并且一旦定义了自己的构造&析构函数, 编译器将不会提供默认的构造&析构函数, 所以如果没有定义无参数的构造函数版本, 创建对象时就必须提供参数!!
所以通常情况下, 在定义了自己的构造函数后, 最好也提供一个 = default的默认构造函数以适应更为复杂的使用环境, 否则可能出现这种情况:
//NoDefault为一个类, 没有默认构造函数
vector<NoDefault> vec(10); //原本应该创建一个含有10个NoDefault类对象的vector
//但是因为NoDefault没有默认构造函数, 所以定义失败
7.1.2: 类的this指针
每个类对象的内部都会有一个隐含的this指针指向该类对象
当调用某个类对象的成员函数时, 编译器会自动将对象本身的地址作为一个隐含参数传递给此函数:
//定义:
class student{
public:
...
string& studentName(void){}
private:
string name;
//调用:
student student1("Lao Wang");
cout<<student1.studentName ()<<endl;
//编译器实际的执行过程类似:
student::studentName(&student1); //这是伪代码
};
默认情况下, this为顶层const指针, 不可以将this指向其他东西
即this的类型为student *const
[拓展: ]关于直接访问类成员&使用this访问的优缺点
-
可以非常明确地指出访问的是调用该函数的对象的成员(增加可读性),且可以在成员函数中使用与数据成员同名的形参(不推荐)
-
不必要的使用, 代码多余
所以整体上个人认为还是使用this访问成员较好, 提升代码可读性, 而付出稍微冗长代价
7.1.2: 常量成员函数
常量成员函数不会修改类对象的数据成员, 其声明方式:
string studentName (void) const;
//在参数列表后加一个const, 告诉编译器&使用者此函数不会修改类对象的数据成员
之前说到了调用类对象的成员函数时都会使用this指针, 所以常量成员函数的本质就是向this指针加入了底层const, 表明不会也无法使用此this修改指向的值, 此时如果在函数内部修改类的数据成员编译器直接报错(防止了意料之外的修改)
[注意:]当类对象时const类型时必须这么整
7.1.3: 定义和类相关的非成员函数
有些函数为相应的类服务, 但是又不属于该类的成员函数(通常将这类函数定义为友元函数), 即他们被定义在了类外部, 这种函数的声明最好和相应类的声明放在同一个头文件中, 这样使用相应类时, 只需要包含一个头文件即可
7.1.3: 关于类输出函数的注意点
类的输出函数应该尽可能的减少格式控制, 给类的使用者更多的自由空间
7.1.3: 应该定义构造函数的情况
- 类中存有必须被初始化的对象:
如: 指针&数组, 按照默认初始化, 这些对象将得到未定义的值, 即成为野指针 - 类中存有其他的类, 且该类没有默认初始化的构造函数:
此时必须定义构造函数, 因为编译器无法合成默认的构造函数
7.1.4: =default & =delete
C++11新特性
上头说道, 当用户定义了构造函数, 编译器就不在合成默认的构造函数, 所以此时类就处在没有默认构造函数的情况, 而此时如果希望编译器仍然为此类合成一个默认的构造函数, 需要加上 =default
相反的,
class student{
public:
student() =default; //让编译器合成一个构造函数, 无需手工定义
student(const string &tempName);//需要手动定义
private:
string name;
};
当然也可以去掉 =default, 此时就需要给出其定义: student(){};
但是这样就使数据成员不在是POD类型, 因此可能让编译器失去对这样的数据类型的优化功能 (编译器会对POD简旧数据类型 (PODType)进行优化, 即O1,O2等级别的优化)
[拓展: ]关于POD简旧数据类型:
1、 所有标量类型(基本类型和指针类型)、POD结构类型、POD联合类型、以及这几种类型的数组、const/volatile修饰的版本都是POD类型。
2、 POD结构/联合类型:一个聚合体(包括class),它的非static成员都不是pointer to class member、pointer to class member function、非POD结构、非POD联合,以及这些类型的数组、引用、const/volatile修饰的版本;
7.1.4: 构造函数的初始化列表
又称为构造函数的冒号初始化:
首先说明, 构造函数内部初始化变量相当于创建变量后再赋值, 而使用冒号初始化相当于创建变量的同时赋值, 即真正的初始化, 但是注意: 不可使用this指针表示类的数据成员如:
rectangle(int x,int y):length(x),width(y){};
// lenth 和 width为 rectangle的两个变量
//参考变量的括号初始化, 即将x和y分别赋值.
冒号初始化的真正顺序与其定义的顺序不同, 其按照被初始化对象在类中的定义顺序初始化, 因此, 在使用一个数据成员初始化另一个时, 就要好好考虑顺序问题了, 所以:
- 最好避免用某些成员初始化其他成员
- 如果必须这么做, 在初始化列表中初始化的顺序要和定义顺序相同
通常以下情况必须用冒号初始化, 其他情况用冒号的效果和普通的赋值运算符初始化相同:
- 对父类进行初始化
调用格式为“子类构造函数 : 父类构造函数”,如下
//其中 QMainWindow是 MyWindow的父类:
MyWindow::MyWindow(QWidget* parent , Qt::WindowFlags flag) :
QMainWindow(parent,flag){}
- 对类中的类成员进行初始化
调用格式为“构造函数 : A(初始值),B(初始值),C(初始值)……”,如下,其中A、B、C分别是类的成员变量:
rectangle::rectangle(int pointX, int pointY, int Width, int Length) :
m_point(pointX,pointY),m_Width(Width),m_Length(Length){…};
- 对类的const成员或引用变量进行初始化
7.1.5: 编译器的所有合成默认操作
与合成的默认构造函数相似, 如果用户没用相应的定义(注意每一个都是单独的, 编译器不会因为用户定义了一个默认构造函数而不自动合成拷贝构造函数), 则编译器会自动合成以下默认函数&操作:
(1)构造函数
(2)析构函数
(3)拷贝构造函数
(4)拷贝赋值函数(operator=)
(5)移动构造函数
(1)operator,
(2)operator &
(3)operator &&
(4)operator *
(5)operator->
(6)operator->*
(7)operator new
(8)operator delete
————————————————
但有些情况下无法使用编译器的合成版本(如上头的构造函数)
7.1.5[拓展]: 关于构造函数的种类和调用
- 默认构造函数: 通常无参数或所有的参数都有缺省值, 并且一个类中只能有一个默认构造函数, 否则将引起冲突, 如:
//二者取其一
CComplex();
CComplex(int i=10,int j=10);
当创建类对象时不提供参数将调用默认构造函数
- 重载构造函数: 最为常见的构造函数, 根据不同类型的参数重载, 如:
CComplex(int nReal, int nImag);
CComplex(double nReal, double nImag);
编译器根据提供的实参类型调用特定的重载构造函数
- 转换构造函数, 即为单参数的重载构造函数
转换构造函数提供了由其参数类型向对应的类类型的隐式转换途径(即隐式类类型转换), 下头会具体介绍
//转换构造函数
CComplex(int nReal);
- 拷贝构造函数, 声明类似于重载构造函数, 但是形参为该类的引用(必须是引用), 如:
CComplex(const CComplex& srcObj);
拷贝构造函数主要应用于使用一个已存在的对象去初始化一个新对象,使新对象的属性和该对象保持一致, 注意: 编译器自动生成的临时对象不能算作是已存在的对象
具体调用拷贝构造函数的情况:
- 使用一个以存在的对象初始化新对象:
CComplex num1(num);
CComplex num2=num; //注意: 这里不会调用operator=重载运算符
再次强调: 这里不会调用operator=重载运算符
- 函数形参为对应类的值传递(其他情况如使用引用或指针时不会发生):
相当于是在函数内部新创建了一个局部变量, 内部实现相当于第一种情况
//按值传递,复制对象
static void TransByValue(CComplex obj)
//传递引用,不复制对象
static void TransByRefence(const CComplex& obj)
- 函数的返回值为对应的类对象(同样返回引用或指针时不会发生):
student returnStu(void){
return *this;
}
stuObj4=stuObj1.returnStu (); //这里会先调用拷贝构造函数, 再调用operator=重载运算符
关于函数返回值的原理, 主要是在返回值寄存器(临时变量)中保存返回值, 当返回的是类对象时, 则在返回值寄存器中构造一个类对象, 所以此时会调用拷贝构造函数
[拓展]: 应该定义拷贝构造函数的情况:
当没有提供拷贝构造函数时, 编译器会合成默认的拷贝构造函数, 但有时候会导致一些问题:
通常, 默认的拷贝构造函数会拷贝类中对应成员的值
当类成员中存在指针时, 其拷贝的就为指针指向的内存地址, 所以这会导致两个不同的类对象中的指针成员指向同一块内存地址(即称作发生了浅拷贝), 当使用其中一个类对象的指针成员操作指向的内存地址时, 另一个对象也会受到影响(有时这是我们不希望发生的, 比如对同一块内存空间两次释放, 会导致程序崩溃)
所以这个时候就需要提供自建的拷贝构造函数
进行深拷贝, 如在拷贝函数中申请新的内存等
class student{
public:
//素质吊差的构造函数:
student(const string &str) {
strptr=(char*)calloc((int)str.size ()+1,sizeof(char));
strncpy(strptr, str.c_str (),str.size ()+1);
return;
}
char *strptr;
//...
}
//素质吊差的main
int main(void)
{
class student stuObj1("WDNMD");
printf("%s\n",stuObj1.strptr);
class student stuObj2(stuObj1);
printf("%s\n",stuObj2.strptr);
scanf("%5s",stuObj1.strptr);
printf("%s\n",stuObj1.strptr);
printf("%s\n",stuObj2.strptr);
return 0;
}
输出结果:
WDNMD
WDNMD
KKSK //输入
KKSK
KKSK //两个都被修改了
[注意: ]特殊情况 1
//类的部分定义:
class student{
public:
student(int n): num(n){
cout<<"Overload Constructor: int\n";
}
student(const student &x) : num(x.num) {
cout<<"Copy Constructor\n";
}
student& operator =(const student &x){
cout<<"Operator =\n";
this->num=x.num;
return *this;
}
//...
int num;
};
这3条语句执行的效果是一样的
class student stuObj1=250; //想使用隐式类类型转换
class student stuObj2=student(250); //想使用operator=
class student stuObj3(student(250));//想使用拷贝构造函数
输出结果:
Overload Constructor: int
Overload Constructor: int
Overload Constructor: int
原因是编译器做了一定的优化
按理来说, 上头的代码至少会调用两次函数, 其中第一次为转换构造函数Overload Constructor: int ,生成一个临时变量, 另一次为operator=重载运算符或拷贝构造函数
但是编译器优化了其中的临时变量的产生, 通通使用转化构造函数直接构造目标对象, 使代码执行的效率更高
因为: 临时对象并非好东西,它是c++程序中很多BUG来源之一,c++编译器会在不影响程序最终执行效果的前提下,尽量减少临时对象的产生, 除非真的绕不开临时变量, 如:
cout<<student(250).num<<endl;
输出结果:
Overload Constructor: int
250
其中编译器构造了一个临时的student类对象(有时称作匿名对象), 此临时对象的生命周期只有一条语句, 作用域也只在这一条语句中
对于函数的返回值问题(在上头)也如此
[注意: ]特殊情况 2
//仍然沿用上头的类定义
class student stuObj2(25);
stuObj1=12345; //关键
cout<<stuObj1<<endl;
如果提供了operator=重载运算符, 则此处使用operator=
如果没有提供operator=重载运算符, 则此处编译器调用的是Overload Constructor: int
(但是此时编译器仍然会合成默认的operator=, 但就是不用, 搞不懂…)
7.2.0: 关于类的public&private
public&private&protected等被称为类的访问说明符
如果类中没有显示的访问说明符, 则所有的成员都是private, 与之相反, struct中所有的对象都是public
且一个类中可以有数量不限的访问说明符, 每个访问说明符的有效范围从其出现到下一个访问说明符或类的末尾截止
7.2.1: 友元函数
之前提到的类相关的非成员函数, 这些函数定义在类之外, 所以只能通过接口访问类的数据对象
如果需要直接访问类的private成员, 则需要将相应的函数定义为友元函数:
class student{
friend string& read(const); //read定义&声明在外部
public:
...
private:
...
};
友元函数可声明在类内的任意位置(包括访问说明符的有效范围内), 但通常集中声明在类的开头或末尾
友元函数的在类内的声明仅仅是表明其为该类的友元函数而已, 无法替代其在类外的声明.
即还需要在类之外声明该函数以确定其作用域
[拓展:]关于友元函数的利弊:
通过友元可以访问到类对象中private成员:
- 增加了程序设计时的自由性
- 同时正因为如此, 友元破坏了类的封装性
利弊同在的友元, 使用时完全看程序员的素质, 但难免会有一些素质吊差的行为, 和全局变量类似, 使用友元时要权衡利弊, 通常推荐只在无路可选的情况下使用友元
7.3.1: 类与别名
别名可以在类中使用, 且会受到访问说明符的限制(注意: #define宏 不行)
可用的有typedef & using
如果定义在private中, 则只有此类的成员可以使用这个别名
如果定义在public中, 则外界可以通过添加此类的类名标定作用域后使用这个别名, 如
//定义
class student{
public:
//二者等价, 选其一即可
typedef string STR;
using STR=vector;
...
private:
...
};
//外界访问:
student::STR wdnmd("wdnmd");
cout<<wdnmd<<endl;
程序输出:
wdnmd
7.3.1: 可变数据成员
可变数据成员(mutable data member)是永远可以被修改的类成员, 有时需要在底层const成员函数中修改某个数据成员的值, 此时可将该数据成员修饰为mutable, 在数据成员前头加上mutable修饰即可:
class student{
public:
...
private:
mutable string name; //此时name变成可变数据成员
...
};
[注意: ]mutable可修饰的只有类中的非const类型和非static类型的数据成员
否则报错
7.3.1: 类中的类数据成员
之前说到, 当一个类作为另一个类的数据成员时, 无法使用编译器合成的默认构造函数初始化, 此时可以选择自建一个构造函数, 用冒号初始化的方法初始化相应的类数据成员, 但是如果需要统一的初始值的话, 更推荐直接声明一个类内初始值, 即:
在数据成员声明时直接在后头用列表初始化或赋值初始化的方法给出初始值:
class Classmates{
public:
...
private:
vector<student> friends{student("Lao Wang")};
...
};
//每个新建的Classmate对象都有一个vector类型的friends
//其中有一个student类型的对象, 被初始化为Lao Wang
[注意: ]这种情况不可使用括号初始化, 会被当成是函数声明, 但是具体原因尚不明确(没百度到令人信服的答案)
7.3.2: (回顾)关于引用类型
C++的引用相当于别名, 与变量名绑定在同一块内存空间上, 使用时与原变量名相同, 别和指针搞混了
int temp0=123;
int &temp1=temp0;
int temp2=temp1; //引用在这个过程中充当传递的角色
// 相当于int temp2=temp0; temp2是temp0的拷贝
printf("%d %d %d\n",temp0,temp1,temp2);
temp2++;
printf("%d %d %d\n",temp0,temp1,temp2);
temp1++;
printf("%d %d %d\n",temp0,temp1,temp2);
输出结果:
123 123 123
123 123 124
124 124 124
而在类中:
//这三个的返回值不同
student* func(void){
return this; //返回当前对象的地址
}
student func(void){
return *this; //返回当前对象的指针
}
student& func(void){
return *this; //返回当前对象的引用(本身)
}
7.3.2:(回顾) 关于点运算符&返回类引用的成员函数
有如下代码:
//其中三个函数都返回的是类的引用
myClass.func1(para1).func2(para2).fun3(para3);
//等效为:
myClass.func1(para1);
myClass.func2(para2);
myClass.func3(para3);
点运算符的执行顺序是从左到右的, 所以上头的func1先执行, 而后返回myClass的引用给func2, 以此类推
7.3.2: const的类成员函数重载
在实例化类时, 会存在const类型&非const类型的类对象:
- 当类的成员函数返回*this时, const类型的对象与非const类型的对象会返回不同的结果, 所以需要为const版本重载一个对应的const类型的函数
- 当类对象为const类型时, this指针也为底层const和顶层const, 所以类中的成员函数都必须为常量成员函数(即在参数列表后加一个const)
如:
class Screen{
public:
Screen & display(std::ostream &os){
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const{
do_display(os);
return *this;
}
const Screen &WDNMD(void) const{
return *this;
}
private:
void do_display(std::ostream &os)const{
os<<contents;
}
int contents=123;
};
int main(void){
Screen screen1;
const Screen screen2;
screen1.display (cout);
screen2.display (cout); //当没有定义const类型的display函数时此行编译错误
screen2.WDNMD (); //当去掉WDNMD的顶层或底层const时此行编译错误
return 0;
}
7.3.3: 关于类对象的声明
//这两种声明方式等效
student student1;
class student student1;
//将class或struct放在前面的做法是从C语言中继承来的
7.3.3: 类的前向声明
类似于函数的声明和定义可以分离, 类也有类似的功能, 称之为前向声明:
class student; //student类的前向声明
前向声明告诉编译器student是一个类名, 以便于在该类真正定义前定义指向该类的指针或引用, 但是由于还未给出其具体的定义, 所以无法进行其他任何操作(包括任何形式的访问)
但是有些编译器会自用优化这方面的代码(应该吧…):
class Y; //按理来说是需要这么一个前向声明的, 但是在Qt中可以删去
class X{
public:
class Y *ptrY=nullptr;
};
class Y{
public:
class X objX;
};
7.3.4: 类的友元(进阶)
之前说到了类的友元函数, 其实也可将其他类定义为某个类的友元, 即友元类
class Screen{
public:
friend class Window_mgr;//Window_mgr类中包含了Screen类
...
private:
...
};
如上代码中, 如果没有将Window_mgr设为Screen的友元, 即使Window_mgr类中包含了Screen类, 也无法使用Screen的private成员
且:
[注意:]友元关系不具有传递性
即Window_mgr的其他友元和Screen不是友元关系
如果需要, 还可将其他类的成员函数设为某一个类的友元, 加上相应的作用域运算符即可, 如
class Screen{
public:
friend void Window_mgr::clear();//clear()此时clear必须已被声明
...
private:
...
};
需要注意的是, 重载函数间互不相同, 如果clear()有其他的重载版本, 则需要根据需求将他们一个个单独加入Screen的友元
7.3.4: 友元声明与作用域
当一个标识符(名字)作为一个友元声明时, 编译器假定该名字在当前的作用域内可见, 所以上头的Window_mgr作为友元时, 可以放在Screen的下头声明
但是作用域运算符不同, 其使用的作用域必须是在当前可见的, 所以上头的
void Window_mgr::clear()作为友元时, Window_mgr必须已经被声明
7.4.1: (回顾)关于typedef & define
二者最大的作用域都只到文件作用域
- define是宏定义, 为文件作用域, 且无法选择
- typedef可以选择作用域: 当其定义在所有函数之外, 则为文件作用域, 而放在函数内则为块作用域
7.4.1: 类中的标识符(名字)查找顺序&成员编译顺序
-
查找顺序遵循: 作用域由内而外, 顺序自上而下
-
编译顺序遵循: 首先编译类中成员的声明(包括数据成员和函数成员),直到类全部可见是再编译函数体
所以, 在定义类时需要注意:
-
在成员函数的定义中可以随意使用类的其他成员而不用担心声明顺序问题
-
同样, 对于类型名, 特别是块作用域的类型别名(typedef&using定义的)最好放在类定义的开头, 以免后头的使用中出现未定义的问题
7.4.1: 类中的标识符命名问题
在类的设计中, 应该避免成员函数的参数等局部变量与类中的数据成员同名, 这会掩盖处在函数外部的数据成员, 但是仍可以通过以下方式使用:
void Screen::dummy_fcn(pos height){
//前一个为函数形参, 后一个为类的数据成员, 两条语句等价
cursor=height*this->height;
cursor=height*Screen::height;
return ;
}
同样如果其他作用域(包括文件作用域)的同名对象也可以使用作用域运算符访问
7.5.2: 委托构造函数
Since C++11
顾名思义, 构造函数将自身的一部分职能委托给其他构造函数完成, 即在构造函数中使用其他构造函数完成构造:
[注意: ]委托构造函数只能委托一个特定的构造函数
class student{
public:
student() : student(""){} //委托构造函数
//将构造的职能委托给下头的构造函数, 并提供一个空的string类型的实参
student(const string &tempName){
this->name=tempName;
return;
}
private:
string name;
};
如果委托函数的函数体非空, 则会先执行函数体中的内容, 然后才将控制权转给委托的函数
所以有:
class student{
public:
student() : student(""){
printf("Void constructor.\n");
return;
} //委托构造函数
//将构造的职能委托给下头的构造函数, 并提供一个空的string类型的实参
student(const string &tempName): student(1) {
printf("String constructor.\n");
return;
}
student (int n){
printf("Int constructor.\n");
return ;
}
};
输出结果为:
Int constructor.
String constructor.
Void constructor.
7.5.4: 隐式的类类型转换
通过构造函数可以定义类类型的隐式转换机制:
class student{
public:
student(const string &tempName){
printf("Object is being created\n");
this->name=tempName;
return;
}
...
private:
...
};
上头的student类的构造函数接收一个string类的参数, 这也表明了允许由string向student类的隐式转换, 即如果编译器需要, 其可以用上头的构造函数将string转换为student类
但是注意的是: 编译器只会自动执行一步隐式类型转换, 即如果将上头的string类替换为原生C字符串, 则编译器只会讲C原生字符串转化为string类, 并不会执行下一步将string转化为student类的转换
==并且, 只有一个参数的
student classmate1=string("Lao Wang"); //正确
student classmate2="Lao Wang"; //错误
student man2=static_cast<string>("Lao Wang"); //正确
student man2=static_cast<string&>("Lao Wang") //错误
//这算作两步转换, 原生字符串->string->string&
注意: 实践证明将对象类型转换为相应的引用也算作一步转换
7.5.4: explicit关键字
- explicit关键字只能用来修饰只有一个参数的类构造函数, 用于表现该构造函数是显示的, 不允许隐式的使用 (功能上与其相对的是implicit关键字, 但是在C++中并不存在, 只有在C#等其他语言中才有)
所以被explicit修饰的构造函数不允许上头的隐式构建类, 包括赋值初始化(即使用等号进行初始化)
所以使用explicit的构造函数只能在上述情况中显示的使用
对于应该使用explicit的构造函数:
对于类の使用中可能发生的迷♂惑行为, 需要将对应的构造函数加个explicit以规范使用
参考vector中的单参数构造函数:
//这个构造函数是explicit
vector<T> name(n); // 创建一个vector,含有n个数据,数据均已缺省构造产生
int getSize(const std::vector<int>&);
getSize(34); //Error: 这样的使用是否显得非常迷♂惑
//当将上头的构造函数定义为explicit后, 只能这么使用:
getSize(vector<int>(20)); //这样看上去就明白多了
所以explicit主要用在类的规范使用优化上
避免使用者产生一些类设计者不希望出现的迷♂惑行为
7.5.6: 字面值常量类
首先解释一下字面值类型:
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”(literal type)
算术类型(即int, double等)、引用和指针都属于字面值类型。某些类也是字面值类型,它们可能含有constexpr函数成员。自定义类Sales_item、IO库、string类型不属于字面值类型。
对于C++的Class, 字面值常量类的特征(必须包含以下条件):
- 数据成员必须都必须是字面值类型
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则初始值必须是常量表达式(在编译期可求值);
如果成员是另一个类, 初始值必须使用该类的constexpr构造函数, 即该类也得是字面值常 量类 - 类必须使用编译器合成的默认析构函数
即可以理解为:
- 满足条件1,就可以在编译阶段求值
- 满足条件2,就可以通过constexpr构造函数创建这个类的constexpr类型的对象
- 满足条件3,就可以保证即使有类内初始化,也可以在编译阶段解决
- 满足条件4,就可以保证析构函数没有不能预期的操作
所以: 满足第一条的struct结构体都是字面值常量类
其中, 重点说明constexpr构造函数, 其必须满足以下几条:
- 必须初始化所有的数据成员
(否则可能出现有些数据成员未被初始化的问题, 这就导致的在编译期有些数据成员无法求值, 与字面值常量的定义相悖) - 函数体必须为空
(标准的constexpr函数只能有一条return语句, 而构造函数没有返回值, 所以就为空了)
所以一个constexpr类在实例化时, 正是通过向constexpr构造函数提供的初始值创建了constexpr类型的类对象, 这使得其在编译期就可求值
字面值常量类的作用:
可以参考constexpr函数的作用:
关于constexpr函数的作用:
- 用于替代C语言中的宏函数(即用define定义的函数, 其本质上还是宏macro)
而关于宏函数的作用, 只能说是具有特定特征的函数被写成了宏函数用于提升程序运行的效率, 所以constexpr函数的作用也类似于此
或者具体的说:
constexpr类的作用类似于const类型的对象, 在程序中作为不可修改的标志等功能, const类型的数据对象是单一存在的, 而constexpr类中的各个数据成员集群存在, 这在需要一类常数时可以较为方便的使用
字面值常量类的使用:
constexpr类可以定义出constexpr的类对象, 也可以定义出非constexpr的类对象:
//当需要constexpr对象时在类对象的定义前加上constexpr修饰即可:
constexpr class Debug myDebugConst(1,0,1);
class Debug myDebugNonConst(1,0,1);
myDebugConst.set_hw (1); //Error: 此为constexpr对象, 不可修改
myDebugNonConst.set_hw (1); //正确, 此为非constexpr对象, 可修改
所以, constexpr类只是为类的使用者提供了constexpr版本, 使用者可以根据需要决定
7.6: 类的静态成员
一个类中的静态成员与这个类本身绑定, 而非与该类的对象绑定, 即一个类中的静态成员被该类的所有对象共用, 其数量不会因为又创建了一个类对象而增加
所以:
当一个类成员需要为整个类服务, 而非某一个类对象时, 优先将其设置为static
保证了功能的前提下, 又将其封装在了类内部, 保护了类的封装性
类的静态成员具有以下几个特点:
- 类的静态成员函数没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数, 并且不可以是底层const (这本身就是修饰this用的)
- 不能将静态成员函数定义为虚函数。(注意:如果在static函数加上virtual关键字就会报出error: Semantic Issue: ‘virtual’ can only appear on non-static member functions)
- 由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember函数指针”(类似于内联函数)
- 与C++中的其他静态变量相同, 静态成员的生命周期伴随着整个程序
静态成员的声明&使用
声明时直接在前头加上static即可
使用时可以通过 类名+作用域运算符 来访问, 也可以通过类对象访问(与普通成员函数相同)
但是还是推荐使用类名+作用域运算符 来访问, 因为使用后者会产生一定的误解, 而前者可以更好的表示其为一个静态成员
静态成员的定义:
对于静态成员函数, 其定义与普通成员函数相同, 可在类内或类外定义, 只是在外部定义时不用加static
对于静态数据成员, 不可通过构造函数初始化, 因为在真正创建类对象前, 静态数据成员就已经存在了, 有两种方法初始化静态数据成员:
- 通常将静态数据成员的初始化放在类之外, 与静态成员函数的操作相同
- 使用类内初始化, 用const类型的值来初始化constexpr类型的静态数据成员(仅限于此)
静态成员&非静态成员的使用区别:
- 一个类的静态成员可以声明为他所属的类类型, 而非静态成员不可
- 类的静态成员可以作为成员函数的参数缺省值, 而非静态成员不可
Extra: 回顾&拓展: C++&C语言中的struct & class
C语言中struct & C艹 中的struct的区别
struct | C语言 | C++ |
---|---|---|
成员 | 没有函数成员只有数据 | 函数和数据都可以有 |
访问权限 | 没有访问权限的设定,及对外不隐藏数据 | 有访问权限的设定private,public,protected |
是否可以继承 | 不可以 | 有继承关系 |
C++中struct和class的区别
C++ | struct | class |
---|---|---|
继承默认权限 | struct默认是public | class默认是private |
定义模板参数 | 不可以 | 可以 |