C++ 基础回顾(下)
目录
- C++ 基础回顾(下)
- 前言
- 模板和泛型编程
- 动态内存与数据结构
- 动态内存
- 数据结构
- 继承与多态
- 继承
- 多态
- 简单的输入输出
- 工具与技术
- 命名空间
- 异常处理
- 多重继承与虚继承
- 时间和日期
前言
C++之前学过一点,但是很长时间都没用过,翻出了书从头看了一遍,简短地做了笔记,以便自己之后查看和学习。这是下篇,上篇链接:C++基础回顾(上)
C++语言中代码复用主要有四种形式:
- 函数,同一个代码模块可以重复调用
- 类,同一个类的不同对象公用同一种数据结构和一组操作
- 继承和多态,派生类可以重复使用基类代码,多态保证代码复用的灵活性
- 模板,泛型编程的基础,不同的实例化版本公用同一份代码设计,是创建类和函数的蓝本
模板和泛型编程
const int& getMax(const int &a,const int &b){
return a>b ?a:b;
}
const string& getMax(const string &a, const string &b){
return a>b?a:b;
} //两段代码中只有数据类型不同
在类中重载函数时,不同的数据类型需要重新定义,要是能够避开参数类型,使用参数类型的时候填充进去就好了。
函数模板就能够解决上面的问题
//getMax函数模板
template <typename T> //模板以关键字template开始,紧跟一个模板参数列表
const T& getMax(const T &a, const T &b){
return a>b?a:b;
}
使用上述函数模板时,需要实例化。
模板的参数类型可以自行推断或者自己显式指明
cout<<getMax(1.0,2.5)<<endl; //推断参数类型T为double
cout<<getMax<double>(1.0, 2.5)<<endl; //显式指明
类成员模板
类的成员函数也可以定义为函数模板
class X{
void *m_p =nullptr;
public:
templarte<typename T>
void reset (T *t){m_p =t;} //成员函数reset定义为一个函数模板
};
可变参函数模板
template <typename...Args> //可变数目的参数称为参数包,用省略号“...”表示,可以包含0~任意个模板参数
void foo(Args ...args){
cout<<sizeof...(args)<<endl; //打印参数包args中参数的个数
}
foo(); //0个参数,输出0
foo(1,1.5); //输出2
foo(1,1.5,"C++"); //输出3
类模板
// 类模板的定义以关键字template开始,后跟模板参数列表
template<typename T, size_t N> //size_t是无符号整数,常用于数组的下标
class Array{
T m_ele[N];
public:
Array(){} //默认构造函数
Array(const std::initializer_list <T> &); //initializer_list是支持有相同类型但数量未知的列表类型,这里没有写形参,应该是一个initializer_list 类型的引用常量
T& operator[](size_t i);
constexpr size_t size() {return N;}
};
//实例化类模板
Array<char,5>a;
Array<int,5>b ={1,2,3};
动态内存与数据结构
内存在对象创建时分配,在相应的时候比如离开作用域时回收内存。但有时候内存大开小用,根本不需要给对象分配这么多的内存,因此动态内存分配技术派上了用场
动态内存
动态对象是在动态内存中创建的,动态内存也称为自由存储区或堆。
new用来分配创建动态对象的内存,delete用来释放动态内存。
int *pi =new int; //在堆中创建一个int类型的对象,把他的地址放到指针对象pi中
delete pi; //delete后跟一个指向动态对象的指针,用来释放动态对象的存储空间
pi = nullptr; //上面pi指向的内存已经被释放了,pi是一个空悬指针。通常是在delete之后重置指针指向nullptr
内存泄漏
使用的过程中,一定避免造成无法释放已经不再使用的内存的情况,这种情况也称为内存泄漏
int i,*q =new int(2);
q =&i; //错误:发生内存泄漏
智能指针
用来解决产生空悬指针或内存泄漏等问题
三种智能指针:
1. unique_ptr 独占所指向的对象,采用直接初始化
2. shared_ptr 允许多个指针指向同一个对象,采用直接初始化
3. weak_ptr 一种不控制所指对象生命期的智能指针,指向shared_ptr所管理的对象
三个指针都是类模板,定义在memory头文件中。
{
unique_ptr <int> p2(new int(207)); //类模板,实例化为int类型的,采用直接初始化
}//p2离开作用域被销毁,同时释放其指向的动态内存,也就是说p2消亡时,其指向的对象也会消亡,动态内存自动释放。
shared_ptr<int> p1(new int(100)); //直接初始化
shared_ptr<int> p1=new int(100); //赋值初始化,错误,只能使用直接初始化
动态数组
int n=5;
int *pa =new int[n]; //方括号中必须是整型,不必是常量,返回第一个元素的地址
delete [] pa; //释放内存,逆向销毁,首先销毁最后一个元素
数据结构
线性链表
线性表在逻辑上和物理结构上都是相邻的。在随机访问数据的时候,可能需要移动很多数据。链式结构则不需要逻辑上相邻的元素在物理结构上也相邻。
线性链表也叫单链表。每个数据元素占用一个结点(node)。一个结点包含一个数据域和一个指针域,其中指针域中存放下一个结点的地址。
单链表利用指针head指向单链表的第一个结点,通过head指针,可以遍历每一个元素,最后一个元素的指针指向为空,尾部的tail指向表尾的结点。
链表中一般有push_back、earse、clear、insert等成员函数。
链栈
栈(stack)只能在一端进行插入和删除操作的线性表。
栈中一般有进栈、出栈、清空、取栈顶元素等操作。
二叉树
一棵非空树有且仅有一个根结点。每个结点的子树的数量为该结点的度(degree)。如结点B的度为3。度为0的结点称为叶子结点,如D、E、H等。
二叉树的定义为每个结点的度不超过2,但是至少要有一个结点的度为2。如下为二叉树的结构。
二叉搜索树,任意一个结点的左子树的数据值都小于该结点的数据值,任意一个结点的右子树的数据值都大于等于该结点的数据值。
继承与多态
继承
继承是代码重用的重要手段之一,可以很容易地定义一个与已有类相似但是又不完全相同的新类。
被继承的称为基类,产生的新类称为派生类。如果一个派生类只有一个基类,称为单继承,如果有多个基类,称为多重继承。
以一个代码例子来讲解其中的原理。
class Person { //基类
protected: //而由protected限定符继承的可以访问其中的受保护成员,但是在派生类外不可访问。
string m_name;//名字
int m_age; //年龄
public:
Person(const string &name = ", int age = 0) :m_name (name), m age(age){}
virtual ~Person()= default; //虚函数,下面有
const string& name ()const{return m name;}
int age()const{ return m_age;}
void plusOneYear(){++m_age;}
void plusOneYear() ++m age; //年龄自增
};
//派生类,需指明基类,形式为" class 派生类名:访问限定符 基类名称{};"
class Student:public Person{ //学生类,公有继承Person 私有的和protected的无法访问,
private:
Course m_course; //课程信息,也是一个类
public:
Student(const string &name, int age, const Course &c):Person(name,age),m_course(c){}
Course course(){return m_course;}
};
派生类对象的构造
Student::Student(const string &name, int age,const course &c):
Person(name,age) /*初始化基类成员*/
m_course(c)/*初始化自有成员*/
{}
多态
多态性包括:编译时多态性和运行时多态性。编译时多态性指的是在程序编译时决定调用哪一个版本的函数,通过函数重载和模板实例化实现。运行时多态是属于一个接口,多种实现。**具体什么意思?**也就是说下面的虚函数,在基类中声明为虚函数是一个接口,在不同的派生类中却有着不同的实现。
虚函数
虚函数表明在不同的派生类中该函数需要不同的实现,因此将该基类中的该函数声明为虚函数。
内联函数、静态成员和模板成员都不能声明为虚函数 。因为这些成员的行为必须在编译时确定,不能实现动态绑定。
简单的输入输出
输入输出过程中,程序在内存中为每一个数据流开辟一个内存缓冲区。在输入操作过程中,从键盘输入的数据先放在键盘的缓冲区中,按回车键时,键盘缓冲区中的数据流到程序的输入缓冲区,形成cin流,然后用输入运算符>>从输入缓冲区中提取数据并将他们保存到与对象相关联的内存中去。输出操作过程中cout<<首先向控制台窗口输出数据时,先将这些数据送到程序中的输出缓冲区保存,知道缓冲区执行刷新操作,缓冲区中的全部数据送到控制台窗口显示出来。
输出缓冲区刷新的原因:缓冲区满、程序正常结束、遇到endl等。endl、flush、ends等都可以强制刷新缓冲区。
cout<<"endl"<<endl; //输出endl和一个换行,然后刷新缓冲区
cout<<"flush"<<flush; //输出flush(无额外字符),然后刷新缓冲区
cout<<"ends"<<ends; //输出ends和一个空字符('\0'),然后刷新缓冲区
//显然他们三个还是有所不同的
空白字符(空格符、制表符、回车符等)
这些字符会在输入时被系统过滤掉,想要获取这些这些字符,可以使用cin.get()
for(char c;(c=cin.get())!='\n') //只要不遇到'\n',就能继续输入
cout<<c;
cout<<endl;
getline(cin,s); //该函数可以获取一行字符序列,遇到回车符结束,不包括回车符
格式化控制
格式化控制可以包括数据的进制、精度和宽度等
//数据进制控制
cout<<showbase<<uppercase; //showbase设置数据输出时显示进制信息,uppercase设置16进制以大写输出并且包含前导字符X
cout<<"default:"<<26<<endl; //默认
cout<<"octal:"<<oct<<26<<endl; //8进制
cout<<"decimal"<<dec<<26<<endl; //10进制
cout<<"hex"<<hex<<26<<endl; //16进制
cout<<noshowbase<<nouppercase<<dec; //输出形式恢复到默认
//上述输出的结果:
default:26
octal:032
decimal:26
hex:0X1A
浮点数格式控制:打印精度、表示形式、没有小数点部分的浮点值是否打印小数点
默认情况下,浮点数按照6位数字精度打印,并以舍入方式而不是截断方式打印。
可以利用setprecision函数或者IO对象的precision函数来指定打印精度。
//设置打印精度
double x = 1.2152;
cout.precision(3); //此处precision接受整型值3,设置精度为3
cout<<"precision:"<<cout.precision()<<",x="<<x<<endl; //此处precision参数为空,返回当前精度
cout<<setprecision(4);
cout<<"precision:"<<cout.precision()<<",x="<<x<<endl;
//输出
precision:3, x=1.22 //四舍五入,不是截断
precision:4, x=1.215
设置输出格式,科学计数法,定点十进制等
cout <<"default format:"<<10*exp(1.0)<<endl; //默认格式
cout <<"scientific:"<<scientific<<10*exp(1.0) <<endl; //科学计数法
cout <<"fixed decimal:"<<fixed<<10*exp(1.0)<<endl; //定点表示
cout <<"default float:"<<defaultfloat <<10*exp(1.0)<<endl; //恢复到默认状态
//输出
default format:27.1828
scientific:2.718282e+01
fixeddecimal:27182818
default float:27.1828
宽度控制,setw
可以指定输入输出数据占用的宽度,setw接受一个int值,若数据宽度大于设定int值,则按照实际输出,若小于,则采用右对齐,左边补空的方式输出。setfill
则可以指定字符填补空白。
int i=-10;
double x=1.2152;
cout <<"i:"<<setw(10)<<i<<endl;
cout <<"x:"<<setw(10)<<x<<endl;
cout <<setfill('*')<<"x:"<<setw(10)<<x<<endl;
//输出
i: -10
x: 1.2152
x:****1.2152
文件流
从键盘(cin)和控制台窗口(cout)是使用的IO流对象,如果要从磁盘读取数据或者向磁盘写入数据则需要文件流。
ifstream //从指定文件读取数据
ofstream //向指定文件写入数据
fstream //可以读写数据
ifstream in(ifname); //创建输入文件流对象in,提供文件名ifname初始化
ofstream out; //创建输出文件流对象,没有提供文件名
out.open(ofname); //没提供文件名的话,可以使用open函数关联一个文件。
if (out) //最好检测open操作是否成功
out.close(); //记得关闭文件
ios::in
以读方式打开文件。
ios::out
以写方式打开文件(默认方式)。如果已有此文件,则将其原有内容全部擦除;如果文件不存在,则建立新文件。
ios::app
以写方式打开文件,写人的数据追加到文件末尾。
ios::ate
打开一个已有的文件,并定位到文件末尾。
ios::binary
以二进制方式打开一个文件,如不指定此方式则默认为 ASCII方式。
工具与技术
命名空间
避免命名冲突,全局作用域分割成为许多子作用域,每个子域为一个命名空间。
namespace Foo {
//放置任何可以放在全局作用域中的声明
}
//命名空间可以是不连续的,在这个文件中命名一点,在另一个文件中再命名一点也可以
//命名空间也可以嵌套
namespace Wang{
namespace Li{
int dosomething(int x,int y);
}
}
//访问时则需要先访问外面的命名空间
int x =Wang::Li::dosomething(1,2);
内联命名空间
内联命名空间可以直接在全局作用域直接访问,而不需要加上命名空间名字
namespace FirstVersion {
void fun(int);
}
inline namespace SecondVersion {
void fun (int);
void fun(double);
}
//调用
FirstVersion::fun(1); //调用早期版本fun函数
fun(1); //调用当前版本fun函数 即second
fun(1.0); //调用当前版本中新增的fun函数,即second
全局命名空间
定义在全局作用域的也就是在全局命名空间的,可以直接使用::
来访问全局命名空间的成员
::memeber_name
异常处理
程序运行过程中可能会出现错误,为了保证大型程序在运行过程中不会出现错误,C++提供了异常的内部处理机制。包含try、catch、throw三个关键字
throw 抛出异常
try 检测可能会出现异常的代码
catch 捕获异常并处理
try检测异常出现后,系统则检查与try对应关联的catch子句,如果找不到则调用标准库中的函数。
double divide (int a, int b){
if(b==0)
throw "Error,division by zero!"; //抛出异常
return a / b;
}
int a=1,b=0;
try {
int c=divide(a,b); //异常检测
}
catch(const string &str){ //异常处理
cerr <<str <<endl; //cerr为标准错误ostream对象,常用于输出程序错误信息
}
catch(const char *str){ //匹配到这个异常处理,但是为什么匹配到这个不太懂,好像懂了,不对,还是不懂,好像是匹配到throw的异常时char风格的字符串
cerr <<str<<endl;
}
标准库异常类,需要包含头文件exception
try{
throw MyException(); //抛出MyException类型的异常对象
}
catch(exception &ex){
cerr <<ex.what() <<endl; //捕获
}
多重继承与虚继承
上文中已经提到多重继承,即一个派生类有多种基类,这也很正常,比如一个“学生”首先可以继承“人”这个类,他也可以继承“孩子”这个类,拥有多重身份。蝙蝠可以继承哺乳类,也可以继承飞行类。
多重继承有可能出现二义性问题。二义性而难题被称为死亡钻石问题。
class Animal{protected: //基类Animal
int m_age;
public:
Animal(int n =0):m_age(n){}
virtual void eat(){}
};
class WingedAnimal:public Animal{ //继承Animal
public:
virtual void feedMilk(){}
};
class Mammal: public Animal{ //继承Animal
public:
virtual void flap(){}
};
class Bat:public WingedAnimal,public WingedAnimal{}; //Bat多重继承
Bat b;
b.eat(); //二义性访问,继承的两个类中都继承了Animal中的eat(),该访问哪一个那
通过虚继承可以解决这样的问题
虚继承是将某派生类的基类声明为基类,那么该基类无论被其他人继承多少次,只共享唯一一份的虚基类成员
class WingedAnimal:virtual public Animal{/*...*/};
class Mammal :virtual public Animal[/* ...*/);
时间和日期
C++提供了对日期和时间进行操作的库chrono。该库中提供三个时钟类system_clock steady_clock high_resolution_clock
时钟类名称 | 实时性 |
---|---|
system_clock | 实时时钟 (随着真实世界的时间调整改变,比如夏令时会把标准时间拨早一小时,有吗?) |
steady_clock | 单调时钟(不随外时间调整而改变) |
high_resolution_clock | 实时时钟(精度更高) |
using namespace chrono;
time_t tt = system clock::to_time_t(system_clock::now());
//to_time_t函数将获取的时间点转换成time_t类型
//now获取其数据成员的时间点time_point
cout <<put time(gmtime(&tt),"F")<<endl;
//gmtime函数将time_t类型函数转换为日历时间
//put_time函数将日历函数转换为时间标准输出流
//输出
2017-12-09 20:15:08
//输出时间间隔
auto start = steady_clock::now();
doSomething(); //执行某种算法
auto end = steady_clock::now();
auto interval = duration_cast <milliseconds > (end - start);
cout <<interval.count () <<endl;
本文的回顾并没有结束,还有一小部分内容没有写出,基本是一些具体函数的使用。
如果您觉得我写的不错,麻烦给我一个免费的赞!如果内容中有错误,也欢迎向我反馈。