c++笔记2(参考learncpp.com)
由于learncpp.com内容过多,此篇博客记录后半部分,前半部分请移步我的博客c++笔记1(参考learncpp.com)
1、函数传参的3种方式,引用实现多返回值函数
3种传参方式:值传递、引用传递、地址传递。
单个基本数据类型,用值传递。
其他类型(string、array等)用引用传递,若不想参数被更改,用const修饰。
引用、指针需要注意的是函数参数必须为左值(如i=5,i是左值,5是右值),因为这两者有“地址”观念。
int foo1(int x); // pass by value
int foo2(int &x); // pass by reference
int foo3(int *x); // pass by address
int i {};
foo1(i); // i不被更改,因为有拷贝
foo2(i); // i可能被改,若不想被改,用int foo2(const int &x);
foo3(&i); // i可能被改
引用实现多返回值函数
#include <iostream>
#include <cmath> // for std::sin() and std::cos()
void getSinCos(double degrees, double &sinOut, double &cosOut)
{
static constexpr double pi { 3.14159265358979323846 }; // the value of pi
double radians = degrees * pi / 180.0; //度转为弧度,如30°→Π/6
sinOut = std::sin(radians);
cosOut = std::cos(radians);
}
int main()
{
double sin(0.0);
double cos(0.0);
getSinCos(30.0, sin, cos);
std::cout << "The sin is " << sin << '\n';
std::cout << "The cos is " << cos << '\n';
return 0;
}
另外可以参考我的另外一篇博客,函数间参数传递的3种方式
2、函数多返回值的3种实现方式
方式一:引用,参考上节内容。OpenCV图像处理框架中常见此用法。
方式二:结构体。
#include <iostream>
struct S
{
int m_x;
double m_y;
};
S returnStruct() //返回结构体
{
S s;
s.m_x = 5;
s.m_y = 6.7;
return s;
}
int main()
{
S s{ returnStruct() };
std::cout << s.m_x << ' ' << s.m_y << '\n';
return 0;
}
方式三:元组 std::tuple。
#include <tuple>
#include <iostream>
std::tuple<int, double> returnTuple() // 返回元组
{
return { 5, 6.7 };
}
int main()
{
std::tuple s{ returnTuple() }; // 调用函数
std::cout << std::get<0>(s) << ' ' << std::get<1>(s) << '\n'; // 用std::get<n>获取元组中元素
/*或者使用如下方式
int a;
double b;
std::tie(a, b) = returnTuple(); // 用std::tie拆解元组
//auto [a,b]{returnTuple()}; // c++17,效果同上,拆解元组
std::cout << a << ' ' << b << '\n';
*/
return 0;
}
3、指向函数地址的指针,std::function的使用
功能:
① 函数指针主要用于在数组(或其他结构)中存储函数,
② 在需要将函数(此函数又称回调函数)传递给另一个函数时。
因为声明函数指针的本机语法很难看而且容易出错,所以我们建议使用std::function。
严格按以下格式定义:
int (*fcnPtr)(); //指针fcnPtr是指向“无参且返回int型”函数的指针,fcnPtr可指向任何同类型的函数。
int (*const fcnPtr)(); //const函数
如下3种等效,fcnPtr指向“两个参数int、double型且返回int型”的函数
int (*fcnPtr)(int,double);
std::function<int(int,double)> fcnPtr; //<返回类型(每个参数类型)>
auto fcnPtr;
#include <iostream>
// #include <functional> //for std::function
int foo(){ return 5;}
int goo(){ return 6;}
void main()
{
int(*fcnPtr)(){&foo}; //指针只能指向“无参且返回int型”的函数
//auto fcnPtr{ &foo }; //使用auto关键字,同上等效
//std::function<int()> fcnPtr{ &foo }; //使用std::function,同上等效
fcnPtr=&goo; //指向函数goo的地址,不需要()
std::cout << fcnPtr(); //隐式使用,更简洁
//std::cout << (*fcnPtr)(); //显式使用,同上等效
}
注意,带默认参数的函数,必须显式地传递任何默认参数的值。默认参数是在编译时解析的,而函数指针是在运行时解析的。因此,当使用函数指针进行函数调用时,无法解析默认参数。
【函数作为参数实现升序、降序】
#include <utility> // for std::swap
#include <iostream>
// 作为参数的函数,又称回调函数,含两个int参数、返回bool。ascending、descending两个回调函数。
void selectionSort(int *array, int size, bool(*comparisonFcn)(int, int))
{
// 前 n-1个元素,与之后的比较大小
for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
{
int bestIndex{ startIndex }; //存储最大或最小元素的下标
// 后n-1个元素
for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
{
if (comparisonFcn(array[bestIndex], array[currentIndex])) // 使用回调函数作为判断条件
{
bestIndex = currentIndex; // 存储最大或最小元素的下标
}
}
// 交换元素,之前是交换下标
std::swap(array[startIndex], array[bestIndex]);
}
}
// 回调函数,bool类型当作触发升序的“开关”
bool ascending(int x, int y)
{
return x > y; // 前>后则返回true,true则触发交换
}
// 回调函数
bool descending(int x, int y)
{
return x < y;
}
void printArray(int *array, int size) //数组会退化为指针,丢失长度信息,所以显式指定
{
for (int index{ 0 }; index < size; ++index)
{
std::cout << array[index] << ' ';
}
std::cout << '\n';
}
int main()
{
int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
// 降序
selectionSort(array, 9, descending);
printArray(array, 9);
// 升序
selectionSort(array, 9, ascending);
printArray(array, 9);
return 0;
}
4、 5个内存区域(又称为段)
The code segment (also called a text segment),代码段(也称为文本段),编译后的程序位于内存中,代码段通常是只读的。
The bss segment (also called the uninitialized data segment),bss段(也称为未初始化数据段),用于存储未初始化的全局变量和静态变量。
The data segment (also called the initialized data segment),数据段(也称为初始化的数据段),用于存储初始化的全局变量和静态变量。
The heap,堆段,从堆中动态分配的变量。
The call stack,调用堆栈,其中存储函数参数、局部变量和其他函数相关信息。
5、std::vector的堆栈操作(Stack behavior)
Stack 是后进先出的结构(LIFO),如果你在堆栈顶部放一个新盘子,从堆栈中移出的第一个盘子将会是你最后推入的盘子。当项目被推入堆栈时,堆栈会变得更大。当项目被弹出时,堆栈会变得更小。
【容量与长度】
int *array{ new int[10] { 1, 2, 3, 4, 5 } }; //容量是10,长度是5。
容量会依据长度自动扩容,但未必会随着长度缩小。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> array{ 0, 1, 2 };
std::cout << "length: " << array.size() << " capacity: " << array.capacity() << '\n';
array.resize(5); // 长度变为5,元素变为0,1,2,0,0
std::cout << "length: " << array.size() << " capacity: " << array.capacity() << '\n';
array.resize(2); // 长度变为2,元素只剩0,1
std::cout << "length: " << array.size() << " capacity: " << array.capacity() << '\n';
}
【堆栈操作】
push_back() 将元素压入堆栈。堆栈变大。
back() 返回堆栈顶部元素的值。堆栈不变,只是看最顶部的元素。
pop_back() 从堆栈中弹出一个元素。堆栈变小。
std::vector<int> stack{};
stack.push_back(5);
stack.push_back(3);
stack.push_back(2); //stack中元素5,3,2
std::cout << "top: " << stack.back() << '\n'; // 2
stack.pop_back(); //拿走最顶部的2,stack中元素剩5,3
最终stack容量为3,长度为2
6、递归与迭代(Recursive vs iterative)
递归就是函数自己调用自己,迭代就是常见的for、while循环遍历。
迭代效率高于递归,因为递归完成前、后,会推入和取出堆栈帧,都会产生一些开销。
如下图所示,每递归一次,count被int一次,新开辟一次内存,这些内存在递归完成后,需要释放掉。stack栈是后进先出的。
代码:
#include <iostream>
void countDown(int count)
{
std::cout << "push " << count << '\n';
if (count > 1) // 终止条件
countDown(count - 1); //递归前、后会推入和取出堆栈帧。因为参数count会被int很多次,即有新内存,这些新内存需要释放掉。
std::cout << "pop " << count << '\n';
}
int main()
{
countDown(5);
return 0;
}
7、assert 与 static_assert
assert语句是一个预处理器宏,它在运行时计算条件表达式。
static_assert的条件部分必须能够在编译时计算。因为static_assert不是在运行时计算的,所以static_assert语句也可以放在代码文件中的任何位置(甚至在全局空间中)。
如果条件表达式为真,则assert语句不执行任何操作。
若为false,则显示一条错误消息并终止程序。此错误消息包含失败的条件表达式,以及代码文件的名称和断言的行号。
断言会损耗一点性能,一般Debug时使用,Release时关闭。IDE一般默认设置了此功能(即宏NDEBUG)
【让断言具有描述性】
assert(found && "你的描述文字"); 或 static_assert(found, "你的描述文字");
原理:"你的描述文字"永远为true,若found为false则触发断言,字符串也会输出出来。
【注意】
exit()函数和assert()函数(如果触发)会立即终止程序,而没有机会做任何进一步的清理(例如关闭文件或数据库)。因此,应该明智地使用它们(仅在程序意外终止而不太可能发生损坏的情况下使用)。
#include <iostream>
#include <cassert> //for assert()
int main()
{
const int i{ 1 }; //若无const,则i在编译时是未知的,运行以后才知道是1
static_assert(i < 0, "必须是负数"); //条件必须是编译时已知的
int j{ 1 };
assert(i < 0 && "必须是负数"); //运行时
return 0;
}
8、Lambda
F.7.15与F.7.16章节暂时搁置,后期再回来更新。
https://www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/
https://www.learncpp.com/cpp-tutorial/lambda-captures/
9、struct与class
在C++中,对只有数据的对象使用struct关键字,对既有数据又有函数的对象使用class关键字。
因为,类会清理自己的内存是合理的(例如,一个分配内存的类会在销毁之前释放内存),但一个结构这样做就不安全了。
注意,struct成员默认public,class成员默认private。
一般将class成员变量设置为私有,成员函数设置为公有,除非您有充分的理由不这样做。
struct DateStruct //类与结构体,在纯数据时几乎无差别。
{
int year{};
int month{};
int day{};
};
class DateClass
{
public:
int m_year{};
int m_month{};
int m_day{};
};
10、类与类的关系——Composition组合
如,游戏中,生物有名称、位置这2个属性,位置可以单独拿出来作为类,成为生物类的一部分。
生物消亡,位置也会消亡。即整体负责局部的释放。
Point2D.h 简单的函数实现直接写头文件里了,复杂的可以写对应的cpp里。
#pragma once
#include<iostream>
class Point2D
{
private:
int m_x;
int m_y;
public:
Point2D():m_x{0},m_y{0} //默认构造
{
}
Point2D(int x, int y) :m_x{ x }, m_y{ y } //含参构造
{
}
void setPoint(int x, int y) //访问函数set、get
{
m_x = x;
m_y = y;
}
int getX () const
{
return m_x;
}
int getY() const
{
return m_y;
}
};
Creature.h
#pragma once
#include<string>
#include"Point2D.h"
class Creature
{
private:
std::string m_name;
Point2D m_location; //点是生物的一部分,生物负责点的消亡。
public:
Creature(std::string name, Point2D location) :m_name{ name }, m_location{ location }
{
}
void moveTo(int x, int y) //生物只需负责运动到哪,无需为创建点担忧
{
m_location.setPoint(x, y);
}
void printMsg()
{
std::cout << m_name << " is at" << '(' << m_location.getX() << ',' << m_location.getY() << ')' << '\n';
}
};
main.cpp
#include <iostream>
#include"Creature.h"
#include"Point2D.h"
int main()
{
std::cout << "Enter a name\n";
std::string name;
std::cin >> name;
Creature creature{ name,{4,7} };
creature.printMsg();
creature.moveTo(5, 8);
creature.printMsg();
return 0;
}
11、类与类的关系——Aggregation聚合
Aggregation聚合,整体不负责局部的创建与释放。局部可以在同一时刻属于不同的整体。
Composition组合,偏向于对象的固有属性,对象不存在,属性也就无意义了(消亡)。
人——出生日期,Composition组合
人——国家,Aggregation聚合
聚合可能更危险,因为聚合不处理其部分的分配。分配由外部方完成。如果外部方不再有指向废弃部分的指针或引用,或者它只是忘记做清理(假设类会处理),那么内存将会泄漏。
12、类与类的关系——Association关联
对象之间属于弱关系,可单向/双向,各自独立。如病人与医生。
通常,应该避免双向关联,因为它们增加了复杂性,而且往往难以不出错地编写。
13、类与类的关系——Dependencies依赖
当一个对象为了完成某些特定任务而调用另一个对象的功能时,就会发生依赖。这是一种比关联更弱的关系,但是,对所依赖的对象的任何更改都可能破坏(依赖的)调用者的功能。依赖关系始终是单向关系。
依赖关系通常不在类级别表示——也就是说,所依赖的对象没有作为成员链接。相反,所依赖的对象通常在需要时实例化(比如打开文件向其写入数据),或者作为参数传递到函数中。
关联是类级别上两个类之间的关系。也就是说,一个类将关联类的直接或间接“链接”作为成员保存。例如,Doctor类有一个指向其患者的指针数组作为成员。
14、类与类的关系——Inheritance继承
类与类间满足is-a,就可以使用继承。如苹果is-a水果。
直接在派生类的初始化列表中赋值基类的成员变量是无效的,可以在初始化列表中使用基类构造函数,其位置并不重要——它总是首先执行。
派生类不能直接访问基类的私有成员,可以使用访问函数来访问。
#include <iostream>
#include <string>
class Base
{
public:
Base(int id=0)
:m_id{id} //构造函数,初始化成员变量
{}
int getId() const{ return m_id; } //访问函数
void setId(int temp) { m_id = temp; }
private:
int m_id;
};
class Derived :public Base
{
public:
Derived(double cost=0.0)
:m_cost{cost}
{}
double getCost() const{ return m_cost; } //访问函数
private:
double m_cost;
};
int main()
{
Derived derived{1.3};
std::cout << derived.getCost() << '\n';
derived.setId(10); //通过基类的访问函数修改基类的成员变量
std::cout << derived.getId() << '\n';
return 0;
}
也可以在派生类的初始化列表中使用基类的构造函数,赋值基类的成员变量
#include <iostream>
#include <string>
class Base
{
public:
Base(int id=0)
:m_id{id} //构造函数,初始化成员变量
{}
int getId() const{ return m_id; } //访问函数
//void setId(int temp) { m_id = temp; }
private:
int m_id;
};
class Derived :public Base
{
public:
Derived(double cost=0.0,int id=0)
:m_cost{cost},
Base{id} //m_id{id}无效,Call Base(int) constructor with value id!
{}
double getCost() const{ return m_cost; } //访问函数
private:
double m_cost;
};
int main()
{
Derived derived{1.3,10}; //省去了derived.setId(10);
std::cout << derived.getCost() << '\n';
std::cout << derived.getId() << '\n';
return 0;
}
15、公有继承、基类成员的访问说明符
继承时推荐公有继承,其他继承参考标题链接。
任何人都可以访问公共成员。
受保护的基类成员可以被派生类直接访问,但不能被公众访问。
私有的基类成员不可被派生类、公众访问。
class Base
{
public: //公有成员
int m_public;
protected: //受保护成员
int m_protected;
private: //私有成员
int m_private;
};
class Derived : public Base //派生类,公有继承
{
public:
Derived()
{
m_public = 1; // 类内可访问
m_protected = 2; // 类内可访问
m_private = 3; // 类内不可访问
}
};
int main()
{
Base base; //Derived derived同理
base.m_public = 1; // 类外可访问
base.m_protected = 2; // 类外不可访问
base.m_private = 3; // 类外不可访问
}
16、基类指针
基类指针等效于基类::,即使指向其他类,调用的仍是基类的成员。
即,基指针或引用只能调用函数的基版本,而不能调用派生版本。
#include<iostream>
#include<string_view>
#include<string>
class Animal
{
public:
Animal(const std::string &name)
:m_name{name}
{}
const std::string &getName() const{return m_name;}
std::string_view speak() const{return "???";}
private:
std::string m_name;
};
class Cat:public Animal
{
public:
Cat(const std::string &name)
:Animal{name} //派生类初始化基类的私有成员变量
{}
std::string_view speak() const {return "Meow";}
};
int main()
{
Cat cat{"Tom"}; //基类的私有成员变量被初始化为Tom
std::cout<<cat.getName()<<" "<<cat.speak()<<'\n';//Tom Meow
Animal *p{&cat}; //因为p是Animal基类的指针,所以p->是Animal::而不是Cat::
std::cout<<p->getName()<<" "<<p->speak()<<'\n'; //Tom ???
return 0;
}
如下是一种让cat发出Meow而不是???的方法,与上述没有本质区别,基类中增加m_speak成员变量,在派生类中初始化它。
#include<iostream>
#include<string_view>
#include<string>
class Animal
{
public:
Animal(const std::string &name,std::string_view speak)
:m_name{name},m_speak{speak}
{}
const std::string &getName() const{return m_name;}
std::string_view speak() const { return m_speak; }
private:
std::string m_name;
std::string_view m_speak;
};
class Cat:public Animal
{
public:
Cat(const std::string &name)
:Animal{name,"Meow"} //派生类初始化基类的私有成员变量
{}
};
int main()
{
Cat cat{"Tom"};
Animal *p{&cat}; //p->依然是Animal::而不是Cat::
std::cout<<p->getName()<<" "<<p->speak()<<'\n'; //Tom Meow
return 0;
}
17、虚函数(c++中重量级内容)
为了解决“基指针或引用只能调用函数的基版本,而不能调用派生版本”问题,虚函数闪亮登场。
虚函数是一种特殊类型的函数,在调用时,它解析为存在于基类和派生类之间的函数的最终派生版本。这种能力称为多态性。
如果派生函数具有与基版本函数相同的签名(名称、参数类型以及是否为常量)和返回类型,则认为该派生函数是匹配的,这样的函数称为覆盖(重写)。
#include<iostream>
#include<string_view>
#include<string>
class Animal
{
public:
virtual std::string_view speak() const{return "???";} //加上virtual关键字
};
class Cat:public Animal
{
public:
virtual std::string_view speak() const {return "Meow";} //加上virtual关键字
};
int main()
{
Cat cat;
Animal *p{&cat};
std::cout<<p->speak()<<'\n'; //Meow,如果上述不加virtual,则???
return 0;
}
通常解析为Animal::speak()。但是,Animal::speak()是虚拟的,它告诉程序去查看基函数和派生函数之间是否有更多派生版本可用,有则用派生版本。在本例中,它将解析为派生的::speak()。
基类、派生类中,虚函数的名称、参数、返回类型必须完全一致。否则即使virtual修饰,也认为是独立的函数。
若基类中函数是虚的,那么派生类中默认此函数也是虚的,不过用virtual修饰是一种好习惯。
class Base //这两个函数是独立的
{
public:
virtual int getValue() const { return 5; }
};
class Derived: public Base
{
public:
virtual double getValue() const { return 6.78; }
};
18、为虚函数而生的override、final关键字
不加override,虚函数也可以运行,但是对于不是真正的虚函数(认为重写了,其实没有),编译器不提醒错误。
class A
{
public:
virtual const char* getName1(int x) { return "A"; }
virtual const char* getName2(int x) { return "A"; }
virtual const char* getName3(int x) { return "A"; }
};
class B : public A
{
public:
virtual const char* getName1(short int x) override { return "B"; } // compile error, function is not an override
virtual const char* getName2(int x) const override { return "B"; } // compile error, function is not an override
virtual const char* getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
};
int main()
{
return 0;
}
若禁止虚函数被重写或类被继承,使用final说明符。若用户试图覆盖已指定为final的函数或类,编译器将给出编译错误。
class A
{
public:
virtual const char* getName() { return "A"; }
};
class B : public A
{
public:
virtual const char* getName() override final { return "B"; } // okay, overrides A::getName()
};
class C : public B
{
public:
virtual const char* getName() override { return "C"; } // compile error: overrides B::getName(), which is final
};
类不可被继承
class A
{
public:
virtual const char* getName() { return "A"; }
};
class B final : public A // note use of final specifier here
{
public:
virtual const char* getName() override { return "B"; }
};
class C : public B // compile error: cannot inherit from final class
{
public:
virtual const char* getName() override { return "C"; }
};
19、虚析构
在17节中讲到,基类、派生类中,虚函数的名称、参数、返回类型必须完全一致。否则即使virtual修饰,也认为是独立的函数。
但有特殊情况,协变返回类型。
#include <iostream>
class Base
{
public:
virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; } //返回指向Base类的指针
void printType() { std::cout << "returned a Base\n"; }
};
class Derived : public Base
{
public:
// 通常,重写,返回类型必须一致,但是Derived继承自Base, 允许写成Derived*,真正返回的依然是Base*
virtual Derived* getThis() override { std::cout << "called Derived::getThis()\n"; return this; }
void printType() { std::cout << "returned a Derived\n"; }
};
int main()
{
Derived d;
Base* b = &d;
d.getThis()->printType(); // 调用Derived::getThis(), 返回Derived*, 调用Derived::printType
b->getThis()->printType(); // 调用Derived::getThis(), 返回Base*, 调用Base::printType
return 0;
}
b->本质是Base::,但Base :: getThis()是虚函数,因此调用Derived :: getThis()。
尽管Derived :: getThis()返回Derived *,Derived *会被向上转换为基版本Base *,因此,将调用Base :: printType()。
虚析构也如此,在处理继承时,应该将任何显式析构函数设为虚函数。
#include <iostream>
class Base
{
public:
virtual ~Base() // note: virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived : public Base
{
private:
int* m_array;
public:
Derived(int length)
: m_array{ new int[length] }
{ }
virtual ~Derived() // 基函数虚,默认也虚。如果~Base()不是虚函数,则认为此函数没有重写~Base()
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived* derived{ new Derived(5) };
Base* base{ derived };
delete base; //若~Base()非虚,则只执行~Base()。否则,先~Derived()再~Base()
return 0;
}
base是一个Base*,当base被删除时,程序会查看~Base()是否为虚函数。若不是,则不会再找其他版本,直接执行~Base()。若是,则执行~Derived()再执行~Base()。
20、纯虚函数(抽象函数)、抽象基类、接口类
纯虚函数使得基类不能被实例化,派生类被迫在实例化这些函数之前定义这些函数。这有助于确保派生类不会忘记重新定义基类所期望的函数。
virtual int getValue() const = 0; //纯虚函数格式
① 任何具有一个或多个纯虚函数的类会变为抽象基类,抽象基类不能实例化(因为其中的纯虚函数它不知道要干什么)。
② 任何派生类都必须为这个函数定义一个主体,否则派生类也将被视为一个抽象基类。
如动物类都会有叫声,具体的派生类才知道(实现)具体的叫声。
virtual const char* speak() = 0; // class Animal
const char* speak() const override { return "Moo"; } //class Cow: public Animal
【接口类】
接口类是没有成员变量的类,其中所有的函数都是纯虚的。
换句话说,这个类纯粹是一个定义,没有实际的实现。
注意,类中要有虚析构。
21、函数模板
只有函数返回类型、参数类型不同,其他样式一致的,可以使用模板。
template <typename T>与template <class T>无区别,推荐typename这个关键字。
由于类型T传入的函数参数可能是类类型,而且按值传递类通常不是一个好主意,因此最好将模板化函数的参数和返回类型设置为常量引用。
#include <iostream>
template <typename T> //声明模板类型参数(单类型)
const T& max(const T& x, const T& y) //函数模板
{
return (x > y) ? x : y;
}
template <typename T1, typename T2> //声明多个类型参数
const T1& min(const T1& x, const T2& y) //函数模板
{
return (x < y) ? x : y;
}
int main()
{
int a = max(3, 7);
std::cout << a << '\n'; //7
int b = min(3, 7.1);
std::cout << b << '\n'; //3
return 0;
}
有时候需要重载操作符,以支持自定义的类
#include <iostream>
template <typename T> //声明模板类型参数
const T& max(const T& x, const T& y) //函数模板
{
return (x > y) ? x : y; // >重载后,可以比较Cents类型的变量
}
class Cents
{
public:
Cents(int cents)
:m_cents{cents}
{}
friend bool operator > (const Cents& c1, const Cents& c2) //重载>
{
return (c1.m_cents > c2.m_cents);
}
friend std::ostream& operator << (std::ostream& out, const Cents& cents) //重载<<,以支持输出Cents类型
{
out << cents.m_cents;
return out;
}
private:
int m_cents;
};
int main()
{
Cents a{ 5 };
Cents b{ 10 };
Cents result{ max(a,b) }; //const Cents& max(const Cents& x,const Cents& y),对于Cents类需重载操作符>
std::cout << result << '\n';
return 0;
}
array中前length项的平均值
#include <iostream>
template <typename T>
T average(T * array, int length)
{
T sum(0);
for (int count{ 0 }; count < length; ++count)
sum += array[count];
sum /= length;
return sum;
}
int main()
{
int array1[]{ 5, 3, 2, 1, 4 };
std::cout << average(array1, 5) << '\n';
double array2[]{ 3.12, 3.45, 9.23, 6.34 };
std::cout << average(array2, 4) << '\n';
return 0;
}
array中自定义类型元素的均值
#include <iostream>
template <typename T>
T average(T * array, int length)
{
T sum(0);
for (int count{ 0 }; count < length; ++count)
sum += array[count]; //自定义类型,需重载 +=
sum /= length; //自定义类型,需重载 /=
return sum;
}
class Cents
{
private:
int m_cents;
public:
Cents(int cents)
: m_cents{ cents }
{
}
friend bool operator>(const Cents& c1, const Cents& c2)
{
return (c1.m_cents > c2.m_cents);
}
friend std::ostream& operator<< (std::ostream& out, const Cents& cents)
{
out << cents.m_cents ;
return out;
}
Cents& operator+=(const Cents& cents) //重载 +=
{
m_cents += cents.m_cents;
return *this;
}
Cents& operator/=(int value) //重载 /=
{
m_cents /= value;
return *this;
}
};
int main()
{
Cents array3[]{ Cents(5), Cents(10), Cents(15), Cents(14) };
std::cout << average(array3, 4) << '\n';
return 0;
}
22、类模板
类模板是实现容器类的理想选择,因为我们非常希望容器能够跨各种数据类型工作,而模板允许您在不复制代码的情况下这样做。尽管语法很难看,错误消息可能很神秘,但模板类确实是c++最好、最有用的特性之一。std::vector 就是经典的类模板。
Array.h,推荐都放到h头文件中,省事方便。
#pragma once
#include <cassert> //断言
template <typename T>
class Array //类,与函数用法一样,将类型替换为T即可
{
private:
int m_length{};
T* m_data{};
public:
Array(int length)
{
assert(length > 0);
m_data = new T[length]{};
m_length = length;
}
Array(const Array&) = delete;
Array& operator=(const Array&) = delete;
~Array()
{
delete[] m_data;
m_data = nullptr;
m_length = 0;
}
T& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; };
};
使用方式
#include <iostream>
#include "Array.h" //引入头文件
int main()
{
Array<int> intArray(12);
Array<double> doubleArray(12);
for (int count{ 0 }; count < intArray.getLength(); ++count)
{
intArray[count] = count;
doubleArray[count] = count + 0.5;
}
for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
return 0;
}
23、异常处理throw、try、catch
try检查异常,throw抛出自定义语句,依据语句的类型跳转到对应的catch去处理异常。如果catch也有异常,那么下一个try去捕获。
#include "math.h" // for sqrt() function
#include <iostream>
int main()
{
std::cout << "Enter a number: ";
double x;
std::cin >> x;
try // 查找try块中异常,并跳转到相应的catch中,触发throw则之后的语句不会执行。
{
if (x < 0.0)
throw "Can not take sqrt of negative number"; // 抛出const char*类型的异常语句
std::cout << "The sqrt of " << x << " is " << sqrt(x) << '\n';
}
catch (const char* exception) // 处理const char*的异常
{
std::cerr << "Error: " << exception << '\n';
}
}
throw不一定在try中, 抛出异常的函数的直接调用者如果不想处理异常,就不必处理异常。可以将任务交给调用者的调用者。一旦有调用者catch处理了这个异常,其他就不再处理。
#include <iostream>
void last() // 被third()调用
{
std::cout << "Start last\n";
std::cout << "last throwing int exception\n";
throw - 1; //触发throw,之后语句不再执行,直接跳转到对应的catch(first函数中有int型异常处理catch)
std::cout << "End last\n";
}
void second() // 被first()调用
{
std::cout << "Start second\n";
try
{
last(); //second()不处理last()throw的异常信息,因为没有匹配的catch。将处理权交给它的调用者first()
}
catch (double)
{
std::cerr << "second caught double exception\n";
}
std::cout << "End second\n";
}
void first() // 被main()调用
{
std::cout << "Start first\n";
try
{
second();
}
catch (int) //last()中throw出int型异常,跳转到此catch。first()的catch处理后,main()不再处理。
{
std::cerr << "first caught int exception\n";
}
catch (double)
{
std::cerr << "first caught double exception\n";
}
std::cout << "End first\n";
}
int main()
{
std::cout << "Start main\n";
try
{
first(); //first()调用second(),second()调用last(),last()中throw出int型异常,second与last都不处理。
}
catch (int) //被first()中的catch (int)处理了,此处不再处理
{
std::cerr << "main caught int exception\n";
}
std::cout << "End main\n";
return 0;
}
24、全捕获处理程序
catch(...) {} //省略号,捕获处理任何类型的异常。
上述语句通常是空的,并且放在所有特定catch类型异常处理的最后,防止特定catch没有考虑全,程序异常终止。
#include <iostream>
int main()
{
try
{
runGame();
}
catch (...) //若runGame()异常,则捕获处理。否则main异常终止。
{
std::cerr << "Abnormal termination\n";
}
saveState(); // 保存用户数据
return 1;
}
25、继承类异常、标准库中的异常
推荐派生类放到基类前面。因为谁在前谁处理。
#include <iostream>
class Base
{
public:
Base() {}
};
class Derived : public Base
{
public:
Derived() {}
};
int main()
{
try
{
throw Derived();
}
catch (const Derived& derived) //推荐派生类放到基类前面。因为谁在前谁处理。
{
std::cerr << "caught Derived";
}
catch (const Base& base)
{
std::cerr << "caught Base";
}
return 0;
}
#include <iostream>
int main()
{
try
{
//throw std::runtime_error("Bad things happened"); //被catch (const std::exception& exception)处理
std::string s;
s.resize(-1); // 触发std::length_error
}
catch (const std::length_error& exception) //我们知道的特定异常
{
std::cerr << "You ran out of memory!" << '\n';
}
catch (const std::exception& exception) //std::exception中包含的异常和std::exception派生类中的异常。
{
std::cerr << "Standard exception: " << exception.what() << '\n';
}
return 0;
}
26、Function try
Function try主要用于在将异常向上传递到堆栈之前记录失败,或者用于更改抛出的异常的类型。对应的catch不处理此异常,自动throw。
#include<iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x(x)
{
if (x <= 0)
throw 1;
}
};
class B : public A
{
public:
B(int x) try: A(x) //Function try
{
}
catch (...) //如果A创建失败,则catch。注意,Function try的catch会自动throw这个异常,它不处理。
{
std::cerr << "Exception caught\n";
}
};
int main()
{
try
{
B b(0);
}
catch (int) //此处最终处理
{
std::cout << "Oops\n";
}
}
27、移动语义,std::move()
移动语义意味着类将转移对象的所有权,而不是复制。移动比复制效率高。
例如不是让复制构造函数或赋值运算符复制指针(“复制语义”),而是将指针的所有权从源转移/移动到目标对象。
std::string str = "Knock"; //str是左值,Knock是右值。
std::move(str); //std::move会将左值变右值,说人话就是直接从str偷走Knock,str里空了。
#include <iostream>
#include <string>
#include <utility> // for std::move
#include <vector>
int main()
{
std::vector<std::string> v;
std::string str = "Knock";
std::cout << "Copying str\n";
v.push_back(str); // 复制str到容器v中——复制语义
std::cout << "str: " << str << '\n';
std::cout << "vector: " << v[0] << '\n';
std::cout << "\nMoving str\n";
v.push_back(std::move(str)); // 直接拿走str中的Knock,放入容器v中——移动语义
std::cout << "str: " << str << '\n'; //str里什么都没有了。注意str依然存在,变成了未初始化状态。
std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';
return 0;
}
移动语义的经典使用场合:
① 许多排序算法(例如选择排序和冒泡排序)都是通过交换元素对来工作的。在以前我们不得不求助于复制语义来做交换。现在我们可以使用move语义,这样效率更高。
② 由一个智能指针管理的内容移动到另一个智能指针。
#include <iostream>
#include <string>
#include <utility> // for std::move
template<class T>
void myswap(T& a, T& b)
{
T tmp { std::move(a) }; // 不复制,直接移动值
a = std::move(b); // a=b会触发拷贝
b = std::move(tmp);
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
myswap(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}