1 C++新语法
C与C++
- 不同的标准库函数
- 不同的编译命令(g++,gcc)
- 指定语言标准(g++ -std=c++20)
C++编译运行
编译:
- 预处理
- #include指令,把相应的头文件插入;
- #define 宏定义,直接对相应的内容进行替换;
- #ifdef #else 根据条件,包含(去除)部分代码;
- 命令:
g++ -std=c++17 –E -P function.cpp –o function.i
- 文法分析
- 汇编
g++ -S -O1 function.i -o function.s
- 函数名称特殊定义,全局变量名称保留,函数和数据放在不同的部分
- 优化
- 代码生成
g++ -c function.s -o function.o
- 使用objdump,readelf查看其内容
objdump –d function.o
链接:
- 命令:
g++ function.o clrmain.o –o main.exe
运行和动态链接:
- 代码公用的部分放在共享动态库中(windows中的dll或linux下的so文件),可减少.exe的文件大小,称为动态链接。
- 通过PATH设置的环境变量去寻找目录
String
std::string s; //未初始化的字符串内容为`""`
std::string t=s;
auto str=std::string(n,c); //用n个字符c来初始化z
const string spaces(greeting.size(), ' ');
编程范式
在命令式程序设计中,控制流程是显式的,程序中的每一条语句表示了一条命令;而其先后关系则指明了执行的顺序关系。
结构化程序设计进行了增强;构成部分包括:包含顺序、选择、循环以及递归四种形式的控制结构
过程化程序设计强调程序的模块化,有时也把过程化程序设计称为模块化程序设计。在不同的语言中,模块的定义是不一样的:例如函数、包、类、对象等。
面向对象程序设计认为现实世界中的问题,实际上是现实世界中的实体(即对象)的相互作用产生的结果。面向对象程序设计涉及到类、类的对象、方法、消息。
在申明式程序设计中,控制流是隐式的但是程序员需要说明最终的结果。没有赋值语句、没有循环,只有SELECT、FROM、WHERE和ORDER等特定的子句(SQL)
函数式程序设计
C++基础类型
C++实体(Entity):不包括宏
整数:
- 后缀是可选的,包括:long(L,l), long-long(LL, ll) unsigned(U, u)
- 可以表示10进制,8进制(前缀为0),16进制(前缀为0x或0X),二进制(前缀为0b或0B)
浮点数:
- 后缀是可选的,包括:float(f, F), double(无后缀), long double(l, L)
nullptr
- C++中NULL被定义为0;
提示:可以使用is_xxx的函数判断给定的类型是否符合条件,需要#include <type_traits>
基本类型: std::is_fundamental
Void
: std::is_voidnullptr_t
: std::is_null_pointer- 整数类型 :std::is_integral,包含布尔、字符、整数类型
- 浮点类型 : std::is_floating_point
- 其中整数类型和浮点类型又称为数值类型: std::is_arithmetic
复合类型:std::is_compound
- 引用类型: std::is_reference
- 指针类型: std::is_pointer
- 成员指针类型: std::is_member_pointer
- 数组类型: std::is_array
- 函数类型: std::is_function
- 枚举类型: std::is_enum
- 类类型: 非union类型std::is_class
- 类类型: union类型 std::is_union
其他类型
- 对象类型(std::is_object):除函数、引用或void类型以外的所有类型
- 标量类型(std::is_scalar):数值类型、枚举类型、指针类型、成员指针类型、nullptr_t
- 对非引用和非函数类型,类型系统支持三种cv限定符,即const、volatitle和const volatile.
typedef和using只是类型的别名
typedef unsigned long ulong;
using ulong = unsigned;
自动类型推导
类型后缀
L:long
LL:long long
U:unsigned
f:float(默认为double)
l:long double
auto(C11及以后)
例子:
- 浮点默认为double
- 字符串默认为const char*
注意:
- 可以使用const,*,&,&&来修饰auto
- auto必须有初始化的值
- 如果使用auto定义多个变量,其推导类型必须一致(如
auto i,j,k
) - auto推导会自动去除const或volatile修饰符
decltype
等价于declare type
decltype
是C++中的一个关键字,用于在编译时确定表达式的类型。它允许您在不实际评估表达式的情况下提取其类型
decltype(表达式)
可以对实体或者表达式进行类型推导。且与auto不一样的是,decltype推导出的类型与实体或表达式的值类型(value category)相关。
-
如果e值类型是xvalue(将亡值), 那么decltype(e)类型为T&&
-
如果e值类型是lvalue(左值), 那么decltype(e)类型为T&
-
如果e值类型是prvalue(纯右值), 那么decltype(e)类型为T
decltype与auto不一样:decltype保留const和volatile
const int ci = 34;
decltype(ci) cj = ci * 2;
decltype(i+ci) ri = i+ci;
decltype(T().begin()) m_it
统一对象初始化:Uniform Initialization
在传统C++代码中,各种对象的初始化差异很大,请看下面的代码
在现代C++代码中,统一使用{}进行初始化
局限:不能进行数据的转换,即发生类型的narrowing时,统一初始化会出错
error: narrowing conversion of ‘4.5e+0’ from ‘double’ to ‘int’ [-Wnarrowing]
控制结构
if语句
带变量初始化的if语句
带constexpr
的if语句
- 当if语句中存在constexpr时,if语句具有编译时执行的特性:即该if语句在编译时执行,如果条件为真,那么假分支将被抛弃;否则真分支将被抛弃。
for语句
支持range-based for语句
for(auto& e:edges){
e.print();
}
对于复杂对象,如果不需要修改,则使用const auto&;否则使用auto &;
for(const auto& student: students){
if (student.name == name)
return true;
}
C++支持结构化绑定(structure binding),使得代码编写更简洁
for(const auto& [id, sname, address]: students){
if (sname == name)
return true;
}
零成本抽象(zero overhead abstraction)–好的语言特性不会导致运行代价;也不要付出代价。
switch语句
不写break;语句会导致fall back through;
强类型枚举:enum class RGB {…}
namespace
名字空间是C++区别C的一个重要方面。
- 名字空间可以避免名字冲突;同时可以把代码组织成模块;
#include <iostream>
namespace fun {
int GetMeaningOfLife(void){
return 8;
}
};
namespace boring {
int GetMeaningOfLife(void){
return 5;
}
};
int main() {
std::cout << boring::GetMeaningOfLife() << '\n'
<< fun::GetMeaningOfLife() << '\n';
}
可以方便地使用自定义的类型。
#include "vec.h"
#include <vector>
int main(){
std::vector<int> v1;
vec::vector<int> v2; //使用自定义的vector类型
}
建议:不要在头文件中使用 using namespace …语句;
- 会污染全局命名空间,可能导致意外的名称冲突。
2 函数
函数声明
形式参数
-
函数外部
-
传值
-
传地址
-
-
函数内部
-
传指针
-
传引用
-
传值方式:首先计算实参的值,然后把实参的值拷贝给形参
传地址方式:实参的地址直接传给形参,在函数内通过地址访问原对象
传指针方式
T* const a; 和 const T* a; T const* a;这三者分别代表什么?
答:1是常量指针,2,3都是指向常量的指针;
const T& 和 T const&是一样的;但是 T& const会报错?
答:引用不是对象,不可被定义为常量
指针问题:
野指针
- 内存的分配与释放必须匹配,如果部匹配就会形成野指针
野指针产生的原因
- 指针定义时没有初始化:指向随机的内存空间;
- 指针释放后没有置空:free§,只是释放内存,并不把p设置为空指针;
- 指针超越变量作用域:局部变量在函数调用后被释放,因此返回函数内的局部变量会产生也指针。
C++中不要使用指针!
- C++中有智能指针作为替代,基本上解决了这些问题!
传引用方式
引用类型(左值引用)
- 任意类型T,其左值引用类型为T&,即在类型后面附加一个&号。
- 左值引用在定义时必须指定初始值,且一旦引用创建之后不可更改:左值引用具有常量的含义。
- 引用不用释放
- 注意:const T& 和 T const&是一样的;但是 T& const会报错。
注意:
- 函数内部的引用和局部变量一样,具有块作用域,在块结束时,编译器自动销毁引用变量;
- 非函数内部的引用和全局变量一样,其生存期是整个程序运行的周期。不建议使用全局引用。
函数参数传递的选择
设计函数,在考虑函数形参类型时,遵循以下规则
- 没有特殊要求的情况下,一概不用指针形参
- 对于数值类型使用传值方式;除非要求修改实参的值,此时使用传引用(T&)方式;
- 对于结构类型、类类型或其他复杂的数据结构,如果无需修改实参,使用const T&类型;如果需要修改实参,使用T&类型。
函数返回值的选择
- 不要使用指针
- 尽量使用T类型作为返回值
- 尽量不要使用引用类型的返回值
函数重载
- 名称相同,但是形参长度或者类型排列不一样的函数
技术手段:名字修饰和重载决议
- 名字修饰:C++编译器在编译阶段把函数名处理成特定的名称:名字前缀和参数编码
- 名字前缀:处理成 _Z8exchange
- 参数编码:基本数据类型有单独的表示;用P代表指针类型、R代表左值类型、O代表右值引用类型,k代表常类型;S_代表当前形参类型与前一形参类型相同(仅在前一类型的描述长度小于2时使用)
- 重载决议(overload resolution)
- 在编译时,对每一个函数调用,执行重载决议;
- 对每一个函数调用,推测其函数实参的类型,找出所有可能的候选函数集合;
- 如果当前候选函数集合仅有一个函数的实现,则选择该函数;否则重载失败:有多个满足条件的函数时,编译器无法决定具体采用哪一种(二义性)。
- 如果定义了很多重载函数而不调用,就不会执行重载决议:尽管某些函数调用会出现代码有问题的情况!
Lambda表达式
// chapt04/lambda_hello.cpp
#include <iostream>
int main(){
[] (auto msg){ std::cout << msg ;
}("Hello world!\n"); //同时实现函数定义和调用
}
默认捕获
- [=] 默认捕获方式为传值捕获
- [&]默认捕获方式为引用捕获
捕获方式可以混合
- [value, &count]
- [[value, this]
- [[&value, &count, &this]
可变规则语法为:Mutable
-
可变规则只影响通过传值方式捕获的变量;
-
如果捕获列表中没有任何传值捕获的对象,那么mutable没有作用;
-
如果捕获列表中有任何传值捕获的对象,则mutable运行这些传值捕获的对象可以在函数体内进行修改;但是这种修改不会改变其值。
#容器内元素排序
using Vector = vector<int>;
Vector sortValues(){
Vector values = {1,2,3,4,5,6,7,8,9};
sort(values.begin(), values.end(),
[](int left, int right){
return (left+1) % 3 < (right+1) % 3;
});
return vlaues;
}
#查找容器内符合条件的元素
void findValues()
{
Vector values = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int target = 8;
auto pos = find_if(values.begin(), values.end(),
[target](int value) //target被捕获
{
return value == target;
});
if (pos != values.end())
cout << "Found\n";
}
函数的定义和申明
定义:完全定义清楚了实体的申明(赋了初值的一定是定义)
什么是申明?
-
没有函数体的函数申明
- int fun(int);
-
申明前带有extern或extern “C”存储标识且没有初始化语句的;
-
extern const int a; 非定义
-
extern const int b=1; 定义
-
-
前向申明(不完全类型申明):struct S;
-
类型别名:typedef类型申明;using类型别名;using申明
变量的定义
- int value;
- int value = 10;
- extern int value = 10;
变量的申明
- extern int value;
ODR原则规定申明可以多次出现,但是定义只能有一次
如果一个对象、引用或函数被 ODR 使用,则其定义必须存在于程序中的某处;否则常为连接时错误。
生存期
静态生存期
- 全局变量
自动生存期
- 局部变量或函数形参
动态生存期
- 使用new或malloc分配的内存,用delete或free进行释放
链接性
无链接
- 局部变量;内部类及成员函数;申明于块内部的类型或变量;
内部链接
- 申明为static的变量、函数或函数模板;
外部链接
- 全局变量、非静态的函数。
3 STL
1.容器
所有STL容器提供基于值的语义而非基于引用的语义:容器管理的是对象,而非指针或引用;
STL容器分类
- 序列容器:array,
vector
, deque,list
, forward_list - 关联容器:
set
,map
, multiset, multimap - 无序关联容器:unorder_* (set, map, multiset, multimap)
- 适配器容器:stack, queue, priority_queue
vector
- 支持随机访问(operate[]操作符)
- array:固定大小的空间
方法:
- 支持array的所有操作
- clear-清除所有元素
- reserve-预留空间
- resize-更改存储元素的格式
- insert,erase
- push_back,pop_back
缺点:
- 从中间插入/删除会移动所有元素
- vector预留的空间不足时,插入元素导致分配新空间以及拷贝所有元素(建议使用
reverse
函数预留空间)
适用对象简单,变化较小,并且频繁随机访问的场景
list
双向链表
方法:
- vector的所有操作,除了at()和operator[]
- push_front:添加到头部
- pop_front:删除头部
优点:
- 插入和删除为O(1)
缺点:
- 不支持元素随机访问:访问头尾O(1),访问中间元素为O(N)
- 不能提前预留空间
set
集合,不允许重复元素(multiset
允许),属于关联容器
对于set/multiset必须定义<(小于操作符),用于比较存放顺序
方法:
- clear
- insert
- erase
- find-搜索元素,返回迭代器
- contains(c++20)-检查元素是否存在于集合中
优点:
- 元素自动排序,且没有重复元素
- 查找效率log2N(使用红黑树实现)
缺点:
- 插入删除效率一般,log2N
适用于需要经常查找元素是否存在,并且需要排序
map
映射,含有Key-value对的已排序映射,要求Key是唯一的(multimap
允许多个相同的键存在)
属于关联容器
对于map/multimap必须定义<操作符:需要使用“小于”关系定义顺序
方法:
- clear
- insert
- erase
- find-返回迭代器
- contains(c++20)
- merge(c++20)-把另一个map的键值对加到当前容器
优点:
- 使用红黑树实现,查找效率为log2N
缺点:
- 插入删除需要调整红黑树,log2N
适用于
- 需要存储一对(key-value)数据
- 要求根据key找value的情景
- 典型应用:单词统计、建立交叉引用列表
关联容器的共同点
包括set/multiset、map/multimap
- 关联容器都需要排序:需要在元素上定义<操作符。同时在插入、删除时,根据该操作符对元素进行排序
- 管理容器的实现以红黑树为基础,因此其查找效率为O(log2N);于此同时,序列容器的查找效率为O(N)
- 关联式容器在每次插入、删除元素时,都必须按照排序规则重新调整红黑树;但是序列容器在每次插入、删除容器中的元素时,往往并不做排序,通常在所有的插入或删除之后进行一次sort,而这种方式往往性能更优
无序关联容器
包括unordered_set/unordered_multiset、unordered_map/unordered_multimap
- 无序关联容器中的元素都是无序的:取消了关联容器中key必须排序的要求
- 无序关联容器的底层实现采用了哈希表的形式,因此需要指定h哈希函数;此外,还要定义=操作符,以确定两个元素是否相等
- 在无序关联容器中,如果不考虑计算key的哈希值的时间,那么查找指定元素的时间复杂度为O(1),远比关联容器的O(log2N)的查找要快
方法:
- 迭代器-只支持begin/end
- 容量-empty,size,max_size,reverse
- 增删-insert,erase,clear,swap,merge,
- extract-提取指定键的迭代器
- 查询-find,contains
- count-返回键的数量
容器适配器
包括stack, queue, 以及priority_queue三大类
借助底层容器(deque或vector)实现,经过封装
stack
后进先出
底层容器默认是deque,需要支持back,push_back,pop_back
方法:
- push
- pop
- top-访问栈顶元素
- empty
queue
先进先出
底层容器默认是list(不能是vector),需要支持back,front,push_back,pop_front
方法:
- push-向队尾压入元素
- pop-从队首取出元素
- back
- front
- empty
容器的共性
- 所有容器提供值语义,而非引用语义。这意味着任何一种类型的容器,都可以像操作整数类型一样,简洁而没有后遗症;
- 元素在容器内部有特定的顺序。同时每种容器都提供迭代器
- 通过for-each遍历容器
- 使用同类型的容器,需要的代码修改可能很少
容器的选择
- 对于元素个数固定的容器,选择array;
- 对于元素个数不固定的容器,选择vector。Vector的内容不结构最简单,支持随机访问,十分灵活;
- 如果经常需要在头、尾插入或移除操作,应该使用deque;如果希望元素被删除时,容器能自动缩减内部使用的内存,也应该采用deque;
- 如果经常需要在容器中执行元素的插入、移动和删除,应该使用list;list在头、尾进行插入或移除,效率高;但是list不支持随机访问;
2.迭代器
该模式提供一种方法,在不暴露对象内部表示的情况下,顺序访问对象中的每一个元素。
迭代器分为5类,分别是
- 输入迭代器: istream
- 输出迭代器: ostream
- 前向迭代器: forward_list, unordered_容器
- 双向迭代器: list, (multi)set, (multi)map
- 随机迭代器: array, vector, deque
万能胶:能够使得容器与算法互不干扰独立发展,最后又能无缝的粘合起来。
逆向迭代器:[ container.rbegin(), container.rend() )这样的半闭半开区间表示
插入迭代器包括:前插迭代器、后插迭代器,以及插入迭代器
// 使用range-based for
for(auto value: a) b.push_back(value);
// 使用copy函数,并使用便捷函数
copy(a.begin(), a.end(), back_inserter(b));
流迭代器
//从标准输出流构造一个输出流迭代器
std::ostream_iterator out_iter {std::cout, " "};
//从标准输入流构造一个输入流迭代器
std::istream_iterator<string> in_iter {std::cin};
std::istream_iterator<string> in_end_iter;
//从标准输入流中读取浮点值,并用它们作为容器中元素的初始
std::vector<double> data;
std::copy(std::istream_iterator<double>{std::cin}, std::istream iterator<double>{}, std::back_inserter(data));
3.算法
算法部分主要头文件包括: algorithm、numeric 和 functional
查找算法——版本4
template <typename Iterator, typename T>
Iterator search(Iterator begin, Iterator end, const T &value)
{
while (begin != end && *begin != value)
++begin;
return begin;
}
模板函数
- 代表一类同构函数,比如search函数,可以针对int\long\float等可以比较的类型。
- 使用关键字template;<>包含模板参数:分为类型模板参数;非类型模板参数;以及模板模板参数。
模板类
- 代表一类同构的类,例如vector,不管里面的元素是哪一种类型,都是用vector表示;
- 使用关键字template;<>包含模板参数:分为类型模板参数;非类型模板参数;以及模板模板参数。
template<class T,class Allocator = std::allocator<T>>
class vector;
4 类
类=属性+方法
通过在参数列表后插入const关键字,可以把成员函数定义为const函数
- 这样的成员函数不能改变调用它们的对象的状态
- const对象只能调用const成员函数
成员函数与非成员函数
三种保护标签:
- struct默认是公有保护
- class默认是私有保护
访问器函数(getter):允许访问私有数据
std::string getname() const{return name;}
函数的申明和定义
- 申明:只明确了接口,含名字、参数及返回值。
- 定义:完整地描述函数的功能,说明了每一步的功能。
class Student_info{
public:
double grade() const;
std::istream & read(std::istream &);//申明
std::string getname() const{return name;}//定义
在函数前加上关键字inline —— 内联函数
- 好处:编译器将函数调用改成函数本体,避免了函数调用开销
检测成员函数是否为空
bool valid()const {return homework.empty();}
class MyClass {
private:
MyClass() { /* 构造函数实现 */ }
public:
static MyClass createInstance() {
return MyClass();
}
};
构造函数
可以显式的调用类的构造函数;创建类的一个对象时,会自动调用适当的构造函数;
如果类中没有定义任何构造函数,那么编译器自动合成一个构造函数。
构造函数重载:一个类可以定义多个构造函数,只要参数个数或者类型不同,确切地说,只要函数的签名不一样就可以。
编程习惯:从任何一个构造函数退出前,确保每个数据成员都有一个有意义的值。
默认构造函数:1)不带有任何参数的构造函数;或者2)所有参数都是默认值参数的构造函数;
Student_info::Student_info() : midterm{0}, final{0} {}
explicit
关键字
防止隐式类型转换
class MyClass {
public:
explicit MyClass(int x) {
// 构造函数的实现
}
};
int main() {
MyClass obj = 10; // 错误: 无法从 'int' 转换为 'MyClass'
MyClass obj2(10); // 正确
return 0;
}
实例:复数类
class Complex
{
private:
double real_{}, imag_{};
public:
double& real() { return real_; }
const double& real() const { return real_; }
double& imag() { return imag_; }
const double& imag() const { return imag_; }
public:
Complex() : Complex(0.f, 0.f) {}
Complex(double real) : Complex(real, 0.f) {}
Complex(double real, double imag) :_real{real}, imag_{imag} { }
Complex(const Complex& th): real_{th._real}, imag_{th._imag} {}
~Complex(){ }
public:
Complex& operator+=(const double& __t);
Complex& operator+=(const Complex& __z);
friend std::istream& operator>>(std::istream& is, Complex& rhs);
friend std::ostream& operator<<(std::ostream& os,const Complex& rhs);
};
友元函数(特殊的非成员函数)
- 友元函数是定义在类的外部,而不是类的内部
- 友元函数可以访问类的所有成员,包括私有和保护成员,就像它们是类的成员一样
- 友元函数的声明通常放在类的内部,使用
friend
关键字 - 友元函数是通常用于运算符的重载
// 一般非成员函数无法访问私有成员
void printData(const MyClass& obj) {
// 错误: 'x' and 'y' are private members of 'MyClass'
// std::cout << "x = " << obj.x << ", y = " << obj.y << std::endl;
}
//友元函数
class MyClass {
friend void printData(const MyClass& obj);
};
void printData(const MyClass& obj) {
// 可以直接访问 MyClass 的私有成员 x 和 y
std::cout << "x = " << obj.x << ", y = " << obj.y << std::endl;
}
非成员函数
std::istream& operator>>(std::istream& is, Complex& rhs)
std::ostream& operator<<(std::ostream& os,const Complex& rhs)
Complex operator+(const Complex& lhs, const Complex& rhs)
Complex operator+(const Complex& __z, const double __t)
Complex operator+(const double __t, const Complex& __z)
bool operator==(const Complex& __x, const Complex& __y)
bool operator!=(const Complex& __x, const Complex& __y)
类的内存管理
对象
-
内存中连续的存储空间;
-
重要的特征:具有内存地址;
函数
函数默认为外部链接性
函数指针:
// 定义两个简单的函数
int add(int a, int b) {
return a + b;
}
int main() {
// 直接定义函数指针
int (*fp1)(int, int);
// 使用 typedef 定义函数指针类型
typedef int (*iifp)(int, int);
iifp fp2;
// 将函数指针指向 add 函数
fp1 = add;
fp2 = add;
// 调用函数指针指向的函数
std::cout << fp1(10, 5) << std::endl; // 输出 15
std::cout << fp2(10, 5) << std::endl; // 输出 15
}
数组
数组名示指向数组首元素的指针;可以通过*(数组名+偏移)访问数组的任意元素;
字符串数组
-
以’\0’作为结束符的字符数组;
-
长度计算时,结束符不计算在内;
-
既然是数组,可以作为迭代器使用,例如 string s(hello,hello+strlen(hello));
函数原型
int main(int argc, char* argv);
- argc指向命令行参数的数量
- argv存放字符串数组
sizeof的使用:获取类型的大小
处理文件
1)可以使用重定向方式;2)或者:使用文件方式。
#includ<fstream>
ifstream infile(“in”);
ofstream outfile(“out”);
Infile和outfile的使用和cin、cout一样。
动态分配
使用new/delete进行内存的动态分配/释放;
C++语言中,分配内存和对象的初始化是紧密联系的:
int *ip = new int(42);
delete ip;
T* p = new T[n];
vector<T> vt<p,p+n);
delete[] vt;
抽象数据类型
正规函数
1)复制构造函数;
2)复制赋值运算符;
3)移动构造函数;
4)移动赋值运算符;
5)析构函数;
6)相等运算符和不等运算符。
class T{
public:
T(const T& t);
const T& operator=(const T& t);
T(T&& t);
const T& operator=(T&& t);
~T();
//...
};
bool operator==(const T& t1, const T& t2);
bool operator!=(const T& t1, const T& t2);
Nice类
定义了上述函数(除不等运算符)的函数称为Nice类
隐式:implicit
- 复制构造函数;
- 复制赋值运算符;
- 移动构造函数;
- 移动赋值运算符;
- 析构函数。
浅拷贝和深拷贝
-
浅拷贝(Shallow Copy):
- 当我们创建一个类型为
X
的对象时,如果采用默认的拷贝构造函数或赋值运算符,会发生浅拷贝。 - 浅拷贝仅复制
X
对象中的指针成员y1
和y2
,但不会复制它们所指向的Y
对象。 - 因此,拷贝后的对象和原对象共享同一个
Y
对象。 - 这意味着,如果其中一个对象修改了
Y
对象,另一个对象也会受到影响。
- 当我们创建一个类型为
-
深拷贝(Deep Copy):
- 为了避免上述问题,我们需要实现一个深拷贝的拷贝构造函数和赋值运算符。
- 在深拷贝中,不仅要复制
X
对象中的成员变量,还要为每个指针成员创建一个新的Y
对象,并将其赋值给拷贝后的对象。 - 这样,拷贝后的对象就拥有了自己独立的
Y
对象,不会受到原对象的影响。
构造vec模板类
template <class T> class Vec{
public:
//接口
private:
T* data;//Vec中的首元素
T* limit;//Vec中的末元素
};
1)类可以控制对象的所有行为
-
创建时:调用构造函数;
-
包含赋值操作的表达式:调用赋值操作符;
-
对象退出或销毁时:自动调用析构函数。
//复制构造函数
Vec(const Vec&v){create(v.begin(),v.end());}
//复制赋值函数
template <class T>
Vec<T>& Vec<T>::operator=(const Vec &rhs)
{
if(&rhs != this){
uncreate();
create(rhs.begin(),rhs.end());
}
return *this;
}
//移动构造函数
Vec(Vec &&v) : _data{v._data}, _limit{v._limit}, _avail{v._avail}
{
v._data = v._limit = v._avail = nullptr;
}
//移动赋值函数
Vec<T>& Vec<T>::operator=(Vec &&v)
{
if(&v != this){
uncreate();
_data = v._data;
_limit = v._limit;
_avail = v._avail;
v._data = v._limit = v._avail = nullptr;
}
return *this;
}
//析构函数
~Vec(){ uncreate(); }
template <class T>
void Vec<T>::uncreate()
{
if (_data!=nullptr)
{
iterator it = _avail;
while (it != _data)
alloc.destroy(--it);
alloc.deallocate(_data, _limit - _data);
}
_data = _limit = _avail = nullptr;
}
//迭代器
typedef T* iterator;
iterator begin(){ return data; }
const_iterator begin()const{ return data; }
//索引
T& operator[](size_type i){return data[i];}
const T&operator[](size_type i) const {
return data[i];
}
类的数值特征
类型转换
自动转换
常见的转换形式 :通过只带有一个参数的构造函数实现。
Str s(“hello”); <=> Str t = “hello”;
如果使用了explicit参数,则不能转换;
- 构造函数的参数是对象的一部分时,不使用explicit;
- 当参数不是对象的一部分时,需要使用explicit。
强制类型转换
operator int() { return data.size();}
野指针问题
class Str {
public:
operator char*();
operator const char*() const;
}
Str s;
ifstream is(s);
ifstream
接受被强制类型转换为const char*
类型的s类
- 这个
const char*
指针指向的内存可能是临时分配的,在Str
对象s
销毁后就会变成野指针。 - 如果
ifstream
的构造函数试图使用这个野指针,就会出现未定义行为,很可能导致程序崩溃。
输入输出操作
- 上面的形式,>>和<<必须是istream和ostream的成员函数
- 如果把>>和<<作为str的成员函数,那么调用形式必须是s>>cin或s<<cout,这个与库规则不合!
std::ostream& operator<<(std::ostream& os, const Str& s)
{
for (Str::size_type i = 0; i != s.size(); ++i)
os << s[i];
return os;
}
问题:这个函数需要访问私有成员数据!
友员函数的访问权限:和类成员函数一样!
friend std::istream& operator>>(std::istream&, Str&);
friend std::ostream& operator<<(std::ostream&, const Str&);
两元运算符
两元运算符设计成对称的;
两元运算符通常设计为友元函数;
Str& operator+=(const Str& s) {
copy(s.data.begin(),s.data.end(),
back_inserter(data));
return *this;
}
Str operator+(const Str& s, const Str& t){
Str r = s;
r += t;
return r;
}
5 面向对象
继承
抽象基类
class Screen
{
public:
virtual void handleInput(sf::RenderWindow& window) = 0;
virtual void update(sf::Time delta) = 0;
virtual void render(sf::RenderWindow& window) = 0;
};
virtual void func() = 0;定义了一个纯虚函数;当一个类中包含纯虚函数时,该类就成为抽象基类。抽象基类不能直接实例化对象,而是用来被继承和实现。
如果派生类没有实现纯虚函数,那么派生类也会成为抽象基类。
继承的可见性
默认private继承
- 子类能继承基类的private对象,但是对于子类不可见,只能通过api访问
- 派生类定义的方法访问基类时:派生类的方法可以访问标记为public、protected、private的方法和属性;
- public-直接原本地继承基类
- protected-将所有的public和protected对象声明为protected
- private-将所有的public和protected对象声明为private
派生类外部函数仅可访问public继承的public函数
class Base {
private:
int privateValue;
protected:
int protectedValue;
public:
Base(int pv, int protv) : privateValue(pv), protectedValue(protv) {}
// 公共成员函数,用于访问 private 成员
int getPrivateValue() const {
return privateValue;
}
};
class Derived : public Base {
public:
Derived(int pv, int protv) : Base(pv, protv) {}
void showValues() {
// 无法直接访问 privateValue
// std::cout << "privateValue: " << privateValue << std::endl; // 编译错误
// 访问 protected 成员
std::cout << "protectedValue: " << protectedValue << std::endl;
// 通过 Base 类的公共成员函数访问 private 成员
std::cout << "privateValue: " << getPrivateValue() << std::endl;
}
};
改变可见性
- 对于外部对象:可以使用friend关键字,通过设置函数或者类为特定类的friend,运行函数或类访问特定类的所有成员。
- 对于派生类:可以使用using Base::f的形式,改变其可见性。
- 仅针对protected对象;在基类中的private成员,不能在派生类中用using声明。
- 可以将基类中的protected对象提升为子类中的public对象
如果希望子类能够访问基类的成员,可以使用
protected
访问修饰符。protected
成员在public继承时对子类是可见的,而对外部类是不可见的。
//visibility.cpp
class Pet{
protected:
char eat() const {
return 'a';
}
int speak() const{
return 2;
}
float sleep() const {
return 3.0;
}
float sleep(int) const {
return 4.0;
}
};
class Goldfish: Pet{
public:
using Pet::eat;
using Pet::sleep;
};
- 严格控制访问:
Goldfish
类不能访问Pet
类的private
成员,但可以通过using
声明将某些protected
成员提升为public
。 - 特化行为:通过选择性地公开
eat
和sleep
函数,而不公开speak
函数,适应不同派生类的需求。
继承的类型
单继承,多继承(多父类),组合继承(先单后多),层次继承(多子类),多级继承(多单继承),混合继承
继承类对象的创建顺序(析构顺序相反)
- 先上后下,先左后右
创建步骤:
- 分配内存空间
- 构造属性对象
- 调用构造函数
类的析构函数是唯一的,仅管理自己的内存
不能继承的方法
- 构造函数,析构函数,赋值操作符-operator=(…)
向上类型转换
取一个对象的地址(指针或者应用),并将其作为基类的地址来进行处理,这种处理方法叫做向上类型转换。
- 将一个类对象的地址绑定到其基类的指针或引用,是合法的
Base* b; //Base class pointer
Derived d; //Derived class object
b = &d;
b->show(); //调用的是基类的方法
绑定
- 把函数调用和函数体相联系(重载)
- 静态绑定(编译)和动态绑定(运行时)
- 动态绑定的例子(虚函数和重载)
多态
- 一个事物具有多种形式
- 通过重写抽象父类函数,派生类函数与父类有相同的名字和签名
- 基类标记为virtual,派生类标记为override
典型的实现
- 编译器对包含虚函数的类创建一个表,通常叫做vtable;
- 在vtable中放置特定类的虚函数的地址;
- 在每个带有虚函数的类中,编译器放置一个指针vptr,让其指向vtable;
- 当通过基类指针或引用调用虚函数时,编译器插入代码,使得能够通过vptr并在vtable中查到到函数,这样能调用正确的函数。
逆向工程原理:动态绑定
切片
对象按值传递时,会切割出一部分,作为基类对象。
如果在派生类中没有把析构函数申明为虚析构函数,那么:在使用向上转换指针或引用时,delete的行为将会只调用基类的析构函数!
因此,在设计具有继承的类时,通常应当把基类的析构函数定义为虚析构函数!
//class Derived1
~Base1(){ cout<<"~Base1()\n";}
//class Derived2
virtual ~Base2(){ cout<<"~Base2()\n";}
Base1* bp = new Derived1;
delete bp;
Base2* b2p = new Derived2;
delete b2p;
bp的删除只调用了基类的析构函数;
而b2p的删除则符合我们的希望:先调用Derived2的析构函数,然后再调用Base2的析构函数!
函数重载
基类和派生类
对于符合重载条件的方法
- 如果不是虚函数,则调用相应对象或指针的函数体(静态绑定)
- 如果是虚函数,在通过指针或引用调用时,调用其实际对象的函数体,即执行动态绑定;
还有一类方法:不符合重载条件的方法,但是同名!
- 函数隐藏:基类中同名的函数,在派生类中都是不可见的!只能使用 base::f()这样的形式进行调用。
组合
描述类对象之间的component-of关系;
例如:汽车由引擎、车门、轮胎等构成。
类的静态成员变量
定义方法:在类中定义静态成员变量时,需加上static关键字。
作用:所有的类对象共享该成员变量。通常与类相关的操作。例如,可以使用类的静态属性跟踪某个类在运行的过程中总共创建了多少个实例;或者为每个类的对象建立一个唯一的id。
类的静态成员变量在使用前必须初始化;且不能在类中直接赋值,需要使用初始化形式(见中间的代码)。
class MyClass {
public:
static int count; // 声明静态成员变量
MyClass() {
count++; // 静态成员变量的使用
}
~MyClass() {
count--;
}
};
// 静态成员变量的初始化
int MyClass::count = 0; // 在类外进行初始化
int main() {
MyClass obj;
std::cout << "Number of objects created: " << MyClass::count << std::endl; // 通过类名访问静态成员变量
return 0;
}
静态成员函数
-
调用:使用类名::静态成员函数()方式或者对象.静态成员函数()。
-
静态成员函数的地址是普通的函数指针;而非静态成员函数的地址则是类成员函数指针(隐含了this指针)
静态成员函数与非静态成员函数调用规则
- 在类的非静态成员函数中可以调用类的静态成员函数;
- 在类的静态成员函数中不能调用类的非静态成员函数;
静态成员函数不能定义为virtual,const和volatile;
例子:单例模式
class Singleton
{
public:
static Singleton* getInstance( );
~Singleton( );
private:
Singleton( );
inline static Singleton* instance(nullptr);
};
Singleton * Singleton::getInstance() {
if (!instance) {
instance = new Singleton();
};
return instance;
};
需要注意的是,这种单例实现在多线程环境下可能会存在线程安全问题,需要额外的同步机制来保证线程安全
补充资料
异常处理
try_catch
try{
...
}
catch(Type1 const& t1){
...
}
catch(Type2 const& t2){
...
}
catch(...){
...
}
catch(…)作为万能异常处理句柄,可以捕获任何类型的异常;
noexcept
告知编译器,函数f和g在执行过程中不会抛出任何未被处理的异常
在函数f和g的内部,它们还是可以抛出异常的,但这些抛出的异常必须在函数内部得到处理。
如果在这些函数中有未被处理的异常,编译时编译器将会发出警告。此外,如果在执行这些函数时发生了未被处理的异常,程序将不会回退到函数f或g的调用处,而是会直接调用terminate()函数
void f(int c)
{
if (c > std::numeric_limits<char>::max()) {
throw std::invalid_argument("f argument too large.");
}
std::cout << "Never show \n";
}
int main() {
try
{
f(256);
}
catch (std::invalid_argument &e) {
std::cout << e.what() << '\n';
return -1;
}
}
//无noexcept
f argument too large.
//有noexcept
terminate called after throwing an instance of 'std::invalid_argument'
what():f argument too large.
noexcept(false)
与之相对的,noexcept(false)则向编译器表明,函数h在执行过程中可能会抛出未被处理的异常。
调用函数h之后仍然存在未被处理的异常,程序的执行会回退到函数h的调用点,并继续进行异常处理,而不是直接调用terminate()函数结束程序的运行。
注意:析构函数默认是不允许抛出未处理的异常的;而构造函数默认是允许抛出未处理的异常的。
~Base() noexcept(false)
#include <iostream>
class Base{
public:
Base() {throw 2023;}
~Base() noexcept(false) {
throw 2024;
}
};
int main(){
try {
Base *b = new Base();
delete b;
}
catch(int const & s) {
std::cout << s << "\n";
}
catch (Base const& b) {
std::cout << &b << std::endl;
}
std::cout << "after delete\n";
return 0;
}
一次try catch只能捕获最先被扔出的异常
上面程序的输出为
2023
after delete
期末考试试卷
1.如果有#include <string>,则以下定义错误的是:
const std::string exclam(5, “!”);
应为const std::string exclam(5, ‘!’);
const int buf; //未初始化
int cnt = 0;
const int sz = cnt;
++cnt; ++sz; //不能对sz进行修改
二、程序阅读题
void f(int& x, int *y, int z) {
z = x + *y / 2;
*y = x + z / 2;
x = *y + z / 2;
}
int main() {
int a = 5, b = 7, c = 11;
f(a, &b, c); //f(5,7,11)
cout << a << ',' << b << ',' << c;
return 0;
}
//函数中z=8,*y=9,x=13,输出为13,9,11
class Base {
char c1, c2;
public:
Base(char n = 'a') :c1(n), c2(n + 2) {}
virtual ~Base() {
cout << c1 << c2 << '\n';
}
};
class Derived :public Base {
char c3;
public:
Derived(char n = 'A') :Base(n + 1), c3(n) {}
~Derived() { cout << c3; }
};
int main() {
Derived* a = new Derived[2];
delete[] a;
}
// Derived(A):Base(B):c1(B),v2(D),c3(A)
//ABD(换行)
//ABD(换行)
class Abc
{
int value_;
static const int SPAN = 0x64;
public:
static int seq;
Abc() :value_(++seq) {}
Abc(const Abc& rhs) :value_(rhs.value_ + SPAN) { ++seq; }
Abc& operator=(const Abc& rhs) {
seq++;
value_ = rhs.value_ / 2;
return *this;
}
int value() const { return value_; }
};
int Abc::seq;
int main()
{
Abc m, n, p, q(p);
m = q;
}
//答案:seq=5 m.value=100+3/2=51
class Value {
int value_;
public:
Value(int v = 0) : value_(v % 7) {}
int value() const { return value_; }
};
bool filter(Value const& v) {
cout << v.value() << ' ';
return v.value() % 5 == 0;
}
void output(Value const& v) {
cout << v.value() << ' ';
}
int main() {
int a[] = { 20, 25, 30, 35, 40, 45, 50 };
vector<Value> values(a, a + sizeof a / sizeof a[0]);
vector<Value> filtered(values.size() / 2);
copy_if(values.begin(), values.end(), back_inserter(filtered),
filter);
cout << '\n';
for (vector<Value>::iterator itr = filtered.begin(); itr !=
filtered.end(); itr++)
output(*itr);
}
//20 25 30 35 40 45 50 %7
//back_inserter作用是在最后插入
//6 4 2 0 5 3 1
//0 0 0 0 5 //前3个是初始化自带的
三、程序填空题
template<class T>//1.
class LoopQueue {
public:
typedef typename vector<T>::size_type size_type;
LoopQueue(int capacity):data(capacity+1)
{
first = last = 0;
}
bool isEmpty()const { return first == last; }
bool isFull()const { return (last + 1) % data.size() == first; }
size_type getLength()const {
if (
last >= first//3.
) return last - first;
return last - first + data.size();
}
bool dequeue(T& e) {
if (isEmpty()) return false;
e=vector[first] //4
;
first=(first+1)%data.size()//5
;
return true;
}
bool enqueue(const T& e) {
if (isFull()) return false;
vector[last]=e //6
;
last=(last+1)%data.size()//7
;
return true;
}
void print() {
size_type i;
for (i = first; i != last; i = (i + 1) % data.size()) {
cout<<"<"<<data[i]<<">";//8
;
}
cout << endl;
}
private:
vector<T> data;
size_type first, last;
};
int main()
{
int a;
LoopQueue<int> qu(3);
for (int i = 1; i < 6; i++) {
qu.enqueue(i);
}
qu.dequeue(a);
cout << qu.getLength() << endl;
qu.enqueue(a);
cout << qu.getLength() << endl;
qu.print();
return 0;
}
四、编程题
template <typename ForwardIterator>
void Rotate(ForwardIterator begin, ForwardIterator mid, ForwardIterator end) {
if (begin == mid || mid == end) return;
// Calculate the distances
//auto f=std::distance(begin,mid);
//auto t=std::distance(begin,end);
std::vector<typename std::iterator_traits<ForwardIterator>::value_type> temp(begin, mid);//typename的作用是告诉编译器后面是类型名
ForwardIterator dest = begin;
ForwardIterator src = mid;
while (src != end) {
*dest++ = std::move(*src++);
}
src = temp.begin();
while (dest != end) {
*dest++ = std::move(*src++);
}
}
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct candidate{
int id;
int score;
};
class candidates{
public:
candidates(){}
void readinfo(istream& in){
candidate c;
while(in>>c.id>>c.score){
candidates.push_back(c);
}
if (in.eof()) {
// Handle end of file gracefully
cout << "End of file reached." << endl;
std::cin.clear(); // Clear fail state
} else if (in.fail()) {
// Handle input failure
cerr << "Input failure occurred." << endl;
}
}
void sort_by_score_ID(){
sort(candidates.begin(),candidates.end(),[](const candidate& c1,const candidate& c2){
if(c1.score>c2.score)
return true;//降序排列
return c1.ID<c2.ID;
});
}
void calculate(int n){
if(n>candidates.size()){
cerr<<"error"<<n<<endl;
exit(1);
}
scoreLine=candidates[n-1].score;
std::copy_if(candidates.begin(),candidates.end(),std::back_inserter(access),[this](const candidate& c){
return c.score>=scoreLine;
});
}
void output(ostream& out){
for(auto&a:access){
out<<a.id<<a.score<<std::endl;
}
}
int get_mps(){
return scoreLine;
}
int get_num(){
return access.size();
}
private:
std::vector<candidate> candidates;
std::vector<candidate> access;
int scoreLine;
};
int main()
{
candidates cs;
cout << "Enter candidates' info: " << endl;
cs.readinfo(cin);
cs.sort_by_score_ID();
cout << "Enter the number of summer camps to be recruited: ";
int n;
cin >> n;
cs.calculate(n);
// cout << "The minimum passing score: " << cs.get_mps() << endl;
// cout << "Number of interviewees: " << cs.get_num() << endl;
cout << "Info of the interviewees: " << endl;
cs.output(cout);
}