1.c++介绍
QMAKE_CXXFLAGS += -std=c++11
1.1.c++语言发展史(了解)
1983年,贝尔实验室(Bell Labs)的Bjarne Stroustrup发明了C++。 C++在C语言的基础上进行了扩充和完善,是一种面向对象程序设计(OOP)语言。
Stroustrup说:“这个名字象征着源自于C语言变化的自然演进”。还处于发展完善阶段时被称为“new C”,之后被称为“C with Class”。C++被视为C语言的上层结构,1983年Rick Mascitti建议使用C++这个名字,就是源于C语言中的“++”操作符(变量自增)。而且在共同的命名约定中,使用“+”以表示增强的程序。
常用于系统开发,引擎开发、嵌入式开发等应用领域, 至今仍然是最受广大程序员喜爱的编程语言之一。
1.2.c++特点(了解)
- 在c的基础上,全面支持面向对象的编程
- 编程领域广泛,功能强大(c++是最难的语言之一)
- c++语言标准一直更新,本次课程使用ISO C++98 、11 标准为主
- 为数不多的支持底层操作的面向对象的语言
- 执行效率高
1.3.面向对象的特点(掌握)
- 类
- 对象
- 封装
- 继承
- 多态
封装 继承 多态 也被称之为面向对象编程的三大特征
1.4.区别(面向对象和面向过程)(熟悉)
【思考】如果把大象装进冰箱,应该怎么做?
- (我)打开冰箱
- (我)把大象放进去
- (我)关闭冰箱
上面的这种方式就是典型的面向过程编程思想,这种思想关注的重点是“过程”,过程指的是一系列有序的步骤,只需要按照这个步骤来做,就可以得到预想的结果。这种思维方式偏向于计算机执行命令的本质。这种程序的特点是执行效率高(因为都是亲历亲为)。适合小体量软件项目编程,偏向性能的项目一般这样做。
面向对象:
- 大象和冰箱拟人化
- 给大象和冰箱分配工作
- 自己执行
1.5.开发环境
单论C++的开发环境,没有严格的要求,为了学习方便,直接使用下门课程的环境进行C++开发,Qt Creator.
软件安装比较简单,只需要在一个不包含中文路径下的文件,一直点击"下一步"即可(组件:全选)。
安装完成:
后期打开此软件的方式:
c++本身不支持中文,要想输出中文,需要更改编码格式
创建项目:
- 创建c++项目,点击
- 按照如下选择
- 填写文件信息
- 后续直接下一步
- 最左侧是文件工程目录
- 文件信息
- 在.pro文件中添加如下代码:
QMAKE_CXXFLAGS += -std=c++11
- 添加完成之后:ctrl+s
- .cpp文件中显示的代码含义
#include <iostream> // 头文件,标准输入输出流
// 一定不要删 使用名字空间
using namespace std;
// 主函数
int main()
{
// 输出hello world! cout输出
// 数据的流向 endl:换行符
cout << "Hello World!" << endl;
// 返回0; 程序结束
return 0;
}
- 运行
点击(ctrl+R)
运行结果:
补充快捷键:
- Alt+0: 显示或者隐藏侧边栏
- ctrl+A:全选 之后:ctrl+i
- ctrl+F:搜索替换
更换主题:
2. 函数概念
本章介绍一些C++拓展的非面向对象的功能。
2.1引用(掌握)
引用在一定程度上是指针的平替,引用相当于是某一变量的“别名”
但引用本身不占用额外的内存空间(除了在某些编译器实现的底层可能有一些微小的区别,但在用户层面可忽略不计),它只是原变量的一个别名。这和指针不同,指针有自己独立的内存空间来存储所指向变量的地址。
定义格式:类型 &引用变量名 = 已经存在的变量
操作引用和操作原变量完全一样,引用变量和原变量地址相同,
注意:当引用指向另外一个引用,实质是指向其代表的变量。
例:本质是int &b=a == int &b=c
int c;
int &a=c;
int &b=a;
//本质是int &b=a == int &b=c
#include <iostream>
using namespace std;
int main()
{
int a=10;
int &b=a;// 给a起一个别名
cout << &a << endl;// 0x61fe88
cout << &b << endl;// 0x61fe88
return 0;
}
注意: 引用的类型和原变量的类型一致
2.1.1 引用的特点
1、一个变量可以取多个别名
2、定义引用时必须初始化,如果不初始化编译器报错,
例:
#include <iostream>
using namespace std;
int main()
{
int a=10;
int &b;// 报错
return 0;
}
3、可以改变引用的值,但是引用不可以成为其他变量的引用
4、声明引用不能赋值NULL
5、声明引用时,引用的值可以为纯数值,这个时候需要const修饰,防止调用后引用更改,实参的值也被更改,
6、可以将变量的地址赋值给一个指针,同样可以使用引用表示地址
int a=100;
int* &b= &a;
7、可以对指针进行引用
8、可以使用const修饰引用,修饰后,不能从该引用更改该值
2.1.2. 引用参数
【思考】
写一个函数,函数有两个参数a和b,函数的功能是交换传入的两个参数原变量的值。
也可以使用异或^完成:
#include <iostream>
using namespace std;
void swap(int *a,int *b)
{
*a=*a^*b;
*b=*a^*b;
*a=*a^*b;
}
void swap1(int &a,int &b)
{
a=a^b;
b=a^b;
a=a^b;
}
int main()
{
int a=10;
int b=20;
swap1(a,b);
cout << "a: " << a << endl;
cout << "b: " << b << endl;
return 0;
}
引用作为参数,在函数参数传递赋值时不会产生副本,相对于指针编写更加简洁,可以使参数传递效率提高。
引用形参,在不参数与计算的情况下,我们建议使用const关键字进行修饰。以达到引用的安全性。
2.2.赋值(熟悉)
使用()或{}进行赋值
2.2.1.()
简单赋值,类型不一样也会强转。
通过=赋值,C++新增赋值方式
2.2.1.{}
类型不一致会报错
c++ 11 中又对上述写法进行升级
注意:在.pro 文件里添加QMAKE_CXXFLAGS += -std=c++11
int num2{5}; // 使用 {} 初始化一个整数
double d2{3.14};
// 以下代码会编译错误,因为存在类型收窄
// int num3{3.14};
2.3.键盘输入(熟悉)
可以使用cin将用户从命令行输入的内容保存到变量中
cin和cout一样属于头文件iostream中的标准输入输出流
getline(字符串输入): 可以输入空格
2.4.string字符串类(掌握)
string不是C++的基本数据类型,它是一个C++标准库中的字符串类,使用时需要引入头文件#include <string>,而不是string.h。
string在绝大多数情况下可以代替C语言中的字符串,不需要担心内存和字符串的长度,因为string内部有很多处理函数可以完成对字符串的操作
2.4.1.获取字符串的长度
字符串变量.size(); / 字符串变量.length();
获取单个字符:可以使用下标的方式
区别:
单个字符的方式更推荐at方法,因为at会更加安全(越界时会有异常),但是[]的效率更高
2.4.2.string支持多种遍历
- 普通循环(for循环)
- C++ 11 :for each循环
- 迭代器(容器章节讲)
2.4.3.字符串和数值型的转换:(了解)
istringstream
是 C++ 标准库中的字符串流类,可用于从字符串中提取各种数据类型。
- 引入头文件<sstream>
- 转换
2.5. 内联函数(掌握)
在普通函数之前加inline
内联函数用于取代C语言中宏定义的函数。内联函数的正确使用可以提升程序的执行效率。内联函数在编译的时候,直接把函数体展开到主函数中编译,在运行时可以减少调用的开销。
通常情况下具有以下特征的函数需要写成内联函数
- 代码长度不超过5行
- 不包复杂的控制语句
- 频繁的被调用
在普通函数之前加inline
#include <iostream>
#include <chrono>
#include <string>
using namespace std;
// 内联函数
inline void print_s(string s)
{
for (int i = 0; i < 1000000; ++i) {
// 空操作,只是为了产生一些延迟
}
cout << s << s << endl;
}
void fun(string s)
{
for (int i = 0; i < 1000000; ++i) {
// 空操作,只是为了产生一些延迟
}
cout << s << s << endl;
}
int main() {
// 获取当前时间点作为开始时间
auto start = chrono::high_resolution_clock::now();
// fun("abc");
print_s("abc");
// 获取当前时间点作为结束时间
auto end = chrono::high_resolution_clock::now();
// 计算持续时间,并转换为毫秒
chrono::duration<double, std::milli> elapsed = end - start;
// 打印结果
cout << "这段代码运行耗时: " << elapsed.count() << " 毫秒" << endl;
return 0;
}
但是手动添加inline关键字只是将函数声明称成内联函数,编译器最终是否按照内联函数运行不一定,编译器有自己的准则,我们只是给编译器提一个建议,具体是否变为内联函数,还是编译器自己决定
2.6. 函数重载overload(重点)
c++中允许多个函数使用同一个函数名,这种用法就是函数重载,重载的函数要求参数不同(类型或者个数),和函数的返回值等其他因素没有关系
除了上述普通函数可以重载外,后续学习的成员函数、构造函数都可以重载,但是析构函数不能重载。(函数重载的中整形数据int的优先级高 浮点型double)
2.7 函数的参数默认值(缺省)值 (掌握)
C++允许给函数的参数设定默认值,调用时如果传入参数,传入的参数会从左到右按照顺序覆盖默认值。所以传入参数的个数可以小于形参的个数。
注意:
- 如果函数需要默认值,函数的声明和定义分开书写,默认值只能出现一次
- 向右(向后)原则,当函数中某一形参有默认值后,后面所有的参数都必须有默认值
- 对于函数默认值,最好不要和函数重载一起使用,因为非常容易出现二义性问题,即使没有二义性问题,也会大幅降低代码的可读性。
2.8. 哑元函数(熟悉)
函数的参数只有类型,没有名称
用途:
- 保证函数的向前兼容性
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
// 哑元函数
void fun(int ,int b)
{
cout << "非常复杂的程序" << b << endl;
}
void fun1()
{
//...
fun(1,2);
}
void fun2()
{
fun(10,20);
}
// ...... 很多地方用到fun
int main()
{
return 0;
}
- 区分函数重载
后期在运算符重载的内容也会使用
3.面向对象基础
3.1.类和对象的概念
类:一个抽象的概念,描述同一类对象的特点。
注意:单纯的类没有任何用处,即只用一个类无法完成任何逻辑处理。
对象:是在类的基础上具象化,从而产生实体。一个类可以有多个对象,不同的对象实体相互独立。
3.2.创建类
类的内容分两个部分
1、属性(名词):通过变量存储:成员变量
2、行为(动词):成员函数。
创建类的语法:
格式:
关键字:class 类名(帕斯卡命名法)
{
// private(私有属性): 里面的属性只能在类里面的函数接口访问或更改。
public(公共开放) :可以外部调用访问赋值
//成员变量;
//成员函数;
};
3.3.创建对象:
c++两种创建方式:
3.3.1.栈内存创建对象
特点:自动创建和销毁,对象访问成员 使用 . 进行访问
3.3.2.堆内存创建对象
创建方法:使用new关键字创建, delete销毁,对象访问成员使用 ->访问
new/delete是C++的运算符
(qt creator中可以写. 自动变成->)
特点:需要手动创建,手动销毁
格式:
堆对象:类名 * 指针对象名 = new 类名 ;
例:
class Luhao
{
public:
int wight;
int hight;
string sex;
string name;
void xuebao(string name)
{
cout <<"雪豹闭嘴"<<name<<endl;
}
void zhishi()
{
cout <<"芝士雪豹"<<endl;
}
void rap()
{
cout<<"今天开始,我要自己上厕所"<<endl;
}
void rap1()
{
cout<<"从今天开始,恕瑞玛让你飞起来!!"<<endl;
}
};
int main()
{
Luhao *jinanfenhao = new Luhao; //创建堆区
jinanfenhao->name="刘宏明";
jinanfenhao->sex="男";
luhaofenhao.hight=185;
luhaofenhao.wight=200;
luhaofenhao.xuebao( jinanfenhao->name);
luhaofenhao.zhishi();
luhaofenhao.rap();
luhaofenhao.rap1();
delete jinanfenhao; //释放
jinanfenhao=NULL; //置NULL
}
栈对象:类名 对象名;
例:
class Luhao
{
public:
int wight;
int hight;
string sex;
string name;
void xuebao(string name)
{
cout <<"雪豹闭嘴"<<name<<endl;
}
void zhishi()
{
cout <<"芝士雪豹"<<endl;
}
void rap()
{
cout<<"今天开始,我要自己上厕所"<<endl;
}
void rap1()
{
cout<<"从今天开始,恕瑞玛让你飞起来!!"<<endl;
}
};
int main()
{
Luhao luhaofenhao;
luhaofenhao.name="陆昊分昊";
luhaofenhao.sex="男";
luhaofenhao.hight=185;
luhaofenhao.wight=200;
luhaofenhao.xuebao(luhaofenhao.name);
luhaofenhao.zhishi();
luhaofenhao.rap();
luhaofenhao.rap1();
}
3.4.封装:
将类中的一些特点(属性、行为)和其他内容隐藏,重新对外提供接口,提高代码的安全性。
关键字:private(私有属性)
格式:
关键字:class 类名(帕斯卡命名法)
{
private(私有属性): 里面的属性只能在类里面的函数接口访问或更改。
public(公共开放) :可以外部调用访问赋值
//成员变量;
//成员函数;
};
3.5.构造函数:
作用:构造函数是一种特殊的成员函数,用于创建对象时做初始化。
写法有一些要求:
- 函数名要和类名一致
- 不写返回值,返回值就是你创造的对象
- 函数名前面不写类型名
- 构造函数如果不写,编译器默认添加一个默认无参函数。
例:
class Luhao
{
private:
int wight= 100;
public:
int hight;
string sex;
string name;
void set_wight (int a)
{
wight = a;
cout<<a<<"的体重";
}
Luhao(string name1,int asd)
{
set_wight(asd);
name=name1;
}
};
特点:
- 构造函数可以重载
- 也可以有参数默认值
3.6. 构造初始化列表
是一种更加简洁给成员变量赋值方式
局部变量和成员变量同名时 1:构造初始化列表 2:可以通过后续的this
3.7. 隐式调用
构造函数调用方式有两种:
显式:在创建对象时手写构造函数的名称
隐式:在创建对象时不手写构造函数的名称也不写参数的列表,编译器会尝试调用对象参数的构造函数(不易读)
explicit 屏蔽隐式调用,不建议使用隐式调用
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string name;
explicit Person(string name):name(name){
cout << "构造函数执行" << endl;
}
};
int main()
{
Person p("zhangsan");// 显式
cout << p.name <<endl;
Person p1=Person("李四");// 显式
cout << p1.name <<endl;
string s="kk"; // 隐式
Person p2=s;
cout << p2.name <<endl;
return 0;
}
3.8.拷贝构造函数
格式: 类名(const 类名 & 引用名 )
可以重载。
当程序员不手写拷贝构造函数时编译器会自动添加一个拷贝构造函数,使对象的创建可以通过这个构造函数实现
#include <iostream>
#include <string>
using namespace std;
class Student
{
private:
string fName;
public:
// 构造函数
explicit Student(string fName):fName(fName){
cout << "构造函数执行 " << endl;
}
// 封装获取
string get_name()
{
return fName;
}
// 编译器添加的拷贝构造函数
Student(const Student & j){
fName=j.fName;
cout << "拷贝构造执行" << endl;
}
};
int main()
{
string k="kang";
Student s1(k);
cout << s1.get_name () <<endl;
Student s2(s1);
cout << s2.get_name () <<endl;
return 0;
}
思考:会不会产生隐患?
存在,当成员变量出现指针类型时,默认的拷贝构造函数会导致两个对象的成员变量指向同一处,不符合面向对象的设计规范,这种现象被称为“浅拷贝”。
3.8.1 浅拷贝
当成员变量出现指针时, 默认的拷贝构造函数会导致创建的对象无法单独持有成员变量的所有权,因此不符合面向对象的封装特性等。
产生的原因:两个成员变量指向了同一个内存空间,导致空间内的信息改变后,所有对象都会发生变化
解决:使每个对象都有自己的内存空间
例:
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Student
{
private:
char * fName;
public:
// 构造函数
explicit Student(char * fName):fName(fName){
cout << "构造函数执行 " << endl;
}
// 封装获取
char * get_name()
{
return fName;
}
// 编译器添加的拷贝构造函数
Student(const Student & j){
fName=j.fName;
cout << "拷贝构造执行" << endl;
}
};
int main()
{
char str[10]="kang";
Student s1(str);
cout << s1.get_name () << endl; // kang
Student s2(s1);
cout << s2.get_name () <<endl; // kang
strcpy(str,"zhang");
cout << s1.get_name () << endl; // zhang
cout << s2.get_name () <<endl; // zhang
return 0;
}
3.8.2 深拷贝
如果无法将指针类型的成员变量更换为非指针,则需要手动编写构造函数,实现当前创建对象的内存开辟,并且这块内存区域归当前创建对象单独持有。即指针指向的地址单独开辟。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Student
{
private:
char * fName;
public:
// 构造函数
explicit Student(char * fName):fName(fName){
cout << "构造函数执行 " << endl;
}
// 封装获取
char * get_name()
{
return fName;
}
// 编译器添加的拷贝构造函数
Student(const Student & j){
fName=new char[10];
strcpy(fName,j.fName);
cout << "拷贝构造执行" << endl;
}
};
int main()
{
char str[10]="kang";
Student s1(str);
cout << s1.get_name () << endl; // kang
Student s2(s1);
cout << s2.get_name () <<endl; // kang
strcpy(str,"zhang");
cout << s1.get_name () << endl; // zhang
cout << s2.get_name () <<endl; // kang
return 0;
}
3.9. 析构函数(掌握)
格式:~ 类名
程序员不写,编译器会自动添加,析构函数会在当前对象销毁时自动调用,用于对象销毁时的资源释放
析构函数是与构造函数对立的函数(严格讲也算成员函数)。
构造函数 | 析构函数 | |
返回值 | 不写,是新创建的对象 | 没有返回值 |
函数名 | 类名 | ~类名 |
功能 | 创建对象并初始化 | 销毁或者释放资源 |
参数 | 可有有,也可以保持默认,可以重载 | 无参数,不可以重载 |
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Dog
{
private:
string name;
public:
Dog(string n):name(n){
cout << name << "构造函数" << endl;
}
// 析构
~Dog()
{
cout << name << "析构函数" << endl;
}
};
int main()
{
{
Dog d1("小黑");
}
Dog *d2=new Dog("小白");
delete d2;
d2=NULL;
cout << "---------------" << endl;
return 0;
}
3.10. 作用域限定符 ::
可以
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
int a=10;
int main()
{
int a=20;
cout << ::a << endl;// 10
return 0;
}
3.10.1 名字空间(熟悉)
名字空间的作用:主要用于解决不同类、函数、常量、变量等命名冲突使用的问题。
定义:namespace 名字空间的名称{ }
使用:using namespace 名字空间的名称;
3.10.2 类内声明 类外定义(掌握)
成员函数与成员变量(通常使用成员函数),可以在类内单独声明,在类外定义,使声明与定义分离。
类内声明 :void 名(参数);
类外定义 :类型 类名::函数名(){};
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Pig
{
private:
string name;
public:
// 类内声明
Pig(string n); //构造函数
string get_name(); //
~Pig();
};
// 类外定义
Pig::Pig(string n)
{
name=n;
}
string Pig::get_name ()
{
return name;
}
Pig::~Pig(){
cout << "24234" << endl;
}
int main()
{
Pig p("小猪");
return 0;
}
在现阶段,偶尔会使用类内声明类外定义的形式,后续课程中可能会强制使用。
3.11.this指针
3.11.1 功能
成员(变量+函数)必须由对象调用。类中成员的调用都依赖于this指针。通常由编译器自动添加。
this指针是一个特殊的指针,指向当前类对象的首地址。
成员函数(包括构造函数与析构函数)中都有this指针。因此this指针只能再类中使用。实际上this指针指向的就是当前运行的成员函数所绑定的对象。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
void fun()
{
cout << this << endl;
}
};
int main()
{
Test t1;
t1.fun (); // 0x61fe8f
cout << &t1 << endl;// 0x61fe8f
cout <<"----------" << endl;
Test * t2=new Test;
t2->fun (); // 0x1162618
cout << t2 << endl; // 0x1162618
delete t2 ;
t2=NULL;
return 0;
}
3.11.2. 区分局部变量和成员变量
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
int a=10;
Test(int a)
{
this->a=a;
}
};
int main()
{
Test t1(200);
cout << t1.a << endl;
return 0;
}
3.11.3 链式调用
支持链式调用的成员函数特点。
- 当一个成员函数的返回值是当前类型的引用时,往往表示这个函数支持链式调用。
- return后面是*this。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
int a;
public:
Test(int a):a(a){}
void get()
{
cout << a << endl;
}
Test & add(int val)
{
a += val;
return *this;
}
};
int main()
{
Test t1(1);
// t1.add (10);
// t1.add(10);
// t1.add(10);
// t1.get ();
// 完成4合1 链式调用
t1.add(10).add(10).add(10).get ();
return 0;
}
3.11.4 配合多态传参
后面学了多态,主要在Qt课程中使用
3.12.static关键字 - 静态(掌握)
1.局部变量 2.成员变量 3.成员函数
3.12.1.静态局部变量
3.12.1.1.使用static关键字修饰局部变量
格式:static 数据类型 变量名。
特点:
- 静态局部变量所在的函数第一次调用时创建,直到程序运行结束后销毁。
- 静态局部变量被当前类的所有对象共用一份。(和C语言修饰局部变量相同)
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
public:
void add()
{
static int a =1;
int b =1;
cout << a++ << " " << &a << endl;
cout << b++ << " " << &b << endl;
}
};
int main()
{
Test t;
t.add();
cout << "-------" << endl;
t.add();
cout << "-------" << endl;
t.add();
Test t1;
t1.add();
cout << "-------" << endl;
t1.add();
return 0;
}
3.12.2. 静态成员变量
格式:static
特点:
- 1、静态成员变量只能在类内声明,类外初始化。
- 2、当前类的所有对象共用一份静态成员变量。(同修饰局部变量相同)
- 3、可以使用< 类名:: >的方式直接脱离对象调用静态成员变量,更推荐使用这种方式调用,因为可读性更好。
- 4、程序开始执行时创建,程序执行结束时销毁。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
public:
static int val;
};
int Test::val=100;
int main()
{
Test t;
cout <<t.val<< endl;
t.val=20;
Test t1;
cout <<t1.val<< endl;
cout << Test::val << endl;
return 0;
}
3.12.3. 静态成员函数
使用static修饰成员函数,这样的函数就是静态成员函数。
特点:
1、可以通过类名直接调用,也可以通过对象调用(推荐使用类名直接调用)。
2、如果函数定义和声明分离(可以不分离),static只需要写在声明处
3、可以脱离对象使用。
4、静态成员函数没有this指针
5、静态成员函数中不能调用同类其他非静态成员,静态成员可以调用静态成员。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
int a=11;
static int val;
static void fun();
};
int Test::val=10;
void Test::fun ()
{
cout << "静态成员函数" << endl;
cout << val << endl;
// cout << a << endl; 非静态成员变量
// cout << this << endl; 报错
}
int main()
{
Test t;
t.fun ();
Test::fun ();
return 0;
}
3.14.4.单例设计模式(了解)
设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。通常用于一些面向对象的语言,如Java、C++、C#等。
本节以一个简化版本的单例设计模式为例,讲解static的实际使用。
#include <iostream>
using namespace std;
/**
* @brief The Singleton class
* 单例模式:只能创建一个对象(可以多次)
*/
class Singleton
{
private:
Singleton(){}
Singleton(const Singleton&);
static Singleton* instance; // 静态成员变量
public:
static Singleton* get_instance() // 静态成员函数
{
if(instance == NULL)
instance = new Singleton;
return instance;
}
static void delete_instance()
{
if(instance != NULL)
{
delete instance;
instance = NULL;
}
}
};
Singleton* Singleton::instance = NULL;
int main()
{
Singleton* s1 = Singleton::get_instance();
Singleton* s2 = Singleton::get_instance();
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
3.13.const (掌握)
用来修饰1.局部变量 2.对象3.成员变量 4.成员函数
3.13.1.修饰的成员函数
const修饰的成员函数,表示常成员函数——常函数。
格式 : 数据类型 函数名()const
特征:
- 可以调用成员变量,但是不能修改这个成员变量的值。(只读不可以修改)
- 不能调用非const的成员函数,哪怕这个函数并没有修改成员变量。
const 修饰成员函数实际上是修饰的this指针指向,this指向的内容不能发生改变
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
public:
int a=10;
void fun() const
{
// a=20; 报错
cout << this->a <<endl;
this->add ();
}
void add() const
{
cout << a << endl;
}
};
int main()
{
Test t;
t.fun ();
return 0;
}
3.13.2. 修饰对象
const修饰对象---常对象
格式:const 类名 对象名;
特点:
- 对象只能访问属性不能修改
- 不能调用非const修饰的函数,常对象只能调用常函数
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
int a=10;
void fun() const
{
cout << a << endl;
}
};
int main()
{
Test t;
t.a=20;
const Test t2;
t2.fun ();
// t2.a=200; 不能修改
cout << t2.a << endl;
return 0;
}
3.13.3. 修饰成员变量
const修饰成员变量,表示该成员变量为常成员变量。表示该成员变量不可被修改
常成员变量一旦初始化(声明后直接初始化 或 构造初始化列表,后者优先级更高),数值不可变。
常成员变量有两种初始化方式:
1.直接赋值
声明后赋值:
2.构造初始化列表
以上两种方式同时使用时,前者失效,以后者为准
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
const int a=20;
Test( int a):a(a)
{
}
void fun()
{
a++;
}
};
int main()
{
Test t(10);
// t.a=200; 报错
cout << t.a << endl;
// t.fun (); 报错
return 0;
}
3.13.4. 修饰局部变量
和C语言相同,常用于函数中的引用参数
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
public:
int a=20;
Test( int a):a(a){ }
void fun()
{
const int b=1;
// b++; 报错
// b=20; 报错
cout << b << endl;
}
};
int main()
{
Test t(10);
// t.a=200; 报错
cout << t.a << endl;
// t.fun (); 报错
return 0;
}
4、运算符重载
4.1.友元(熟悉)
类实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,仅能通过类的成员函数才能读写。如果数据成员定义为公共的,则又破坏了封装性。但是某些情况下,需要频繁读写类的数据成员,特别是在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,而影响程序的运行效率。
缺点:维护性变差
类:实现数据的隐藏和封装,私有的数据只能通过类内的成员函数才可以访问
友元:是一种允许非成员函数(非当前类的成员函数)访问类内私有成员一种方式,破坏类的封装性,但是可以提高代码的运行效率
友元函数实现方式:
- 友元函数
- 友元类
- 友元成员函数
友元可以访问类的所有成员
4.1.2. 友元函数
格式:friend 函数类型 函数名(形参);
友元函数不属于任何一个类,是一个类外的函数,但是在类内进行“声明”。虽然友元函数不是类中的函数,但是却可以访问类中的所有成员。
例:生活中 客厅 卧室
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Room
{
private:
string bed_room;
public:
string living_room;
void fun()
{
cout << bed_room << endl;
cout << living_room << endl;
}
Room(){
living_room="客厅";
bed_room="卧室";
}
// 声明 某某函数是我的好朋友
friend void func(Room & b);
};
void func(Room & b)
{
cout << b.bed_room << endl;
cout << b.living_room << endl;
}
int main()
{
Room p;
func(p);
return 0;
}
友元函数使用的注意点:
- 友元函数没有this
不能放在函数里面
- 友元函数声明时可以放置到类的任何位置,不受类的权限修饰符的影响
- 一个友元函数可以成为多个类的"好朋友"(理论上讲可以访问多个类),每个类中都要声明
4.1.3. 友元类
格式:friend class 类名;
当一个类B成为了另一个类Test的“朋友”时,类test的所有成员都可以被类B访问,此时类B就是类Test的友元类。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
int a;
public:
Test(int a ):a(a)
{
}
void print_a()
{
cout << a << " " << &a << endl;
}
// 声明B是好朋友
friend class B;
};
class B
{
public:
void test_friend(Test & t)
{
cout << t.a << " " << &t.a << endl;
}
};
int main()
{
B b;
Test t1(10);
b.test_friend (t1);
t1.print_a ();
return 0;
}
注意:
- 友元关系不具有交换性
- 友元关系不能继承
4.1.4. 友元成员函数
概念: 那个类的成员函数可以访问本类的封装内容。
步骤:
- 被访问类需声明访问的友元成员函数为 friend 类型
- 格式:friend 访问类名::函数名();
- 被访问类需类内声明类外定义(把友元成员函数 进行类外定义)
- 访问的类需要声明该友元成员函数
- 将被访问的类进行声明
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
// 4. 将被访问的类进行声明
class Test;
class B
{
public:
// 3. 类内声明类外定义(友元成员函数类外定义)
void test_friend(Test & t);
// void test_friend1(Test & t)
// {
// cout << t.a << " " << &t.a << endl;
// }
};
class Test
{
private:
int a;
public:
Test(int a ):a(a) {}
void print_a()
{
cout << a << " " << &a << endl;
}
// 1.声明B的成员函数是好朋友
friend void B::test_friend (Test & t);
};
// 2.
void B::test_friend (Test & t)
{
cout << t.a << " " << &t.a << endl;
}
int main()
{
B b;
Test t1(10);
b.test_friend (t1);
// b.test_friend1 (t1); 报错
return 0;
}
4.2.运算符重载(掌握)
4.2.1 概念
c++中可以把部分的运算符当做是函数,此时运算符就可以进行重载,本质就是函数的调用
运算符预定义的操作只能针对基本数据类型,但是对于自定义类型,也需要类似的运算操作,此时就可以重新定义这些运算符的功能,使其支持特定类型,完成特定的操作。
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
int a;
public:
Test(int a):a(a){}
int get_a()
{
return a;
}
};
int main()
{
Test t1(1);
Test t2(2);
Test t3=t1+t2;
cout << t3.get_a () << endl;
return 0;
}
运算符重载实现方式:
- 友元函数运算符重载
- 成员函数运算符重载
4.2.2. 友元函数运算符重载
格式: 类名 operator 运算符(自定义的参数1,参数2)
对于自增自减,前置参数2可以不写,后置参数二要变成哑元函数
以双目为例: 当两个自定义的数据类型相加时
operator + :函数名
- 声明友元函数: friend 类名 operator + (自定义的参数1,参数2);
- 实现 类外定义函数
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
class Test
{
private:
int a;
public:
Test(int a):a(a){}
int get_a()
{
return a;
}
friend Test operator +(Test & t1,Test & t2);
};
Test operator +(Test & t1,Test & t2)
{
// Test t3= t1.a+t2.a;
// return t3;
// 隐式调用构造函数
return t1.a+t2.a;
}
int main()
{
Test t1(1);
Test t2(2);
Test t3=t1+t2;
cout << t3.get_a () << endl;
return 0;
}
单目运算符:
后置++需要加一个哑元函数 例:
例:
class Test
{
private:
string secret_room="卧室";
string other_room="客厅";
public:
int a=0;
Test(int b)
{
a=b;
cout <<a<<endl;
}
friend void B::nihao(Test & p);
};
Test operator ++(Test & p)
{
return ++p.a;
}
void B::nihao(Test & p)
{
cout <<p.other_room<<endl;
}
int main()
{
Test buf(10);
Test buf1=++buf;
B asd;
asd.nihao(buf);
return 0;
}
4.2.3 成员函数运算符重载
成员函数运算符重载相比于友元函数运算符重载,最主要的区别在于,成员函数比友元函数参数少一个,在成员函数中使用this指针代替。代替的为符号左边。
class Test
{
private:
public:
int a=0;
Test(int b)
{
a=b;
cout <<a<<endl;
}
friend void B::nihao(Test & p);
Test operator -(Test & p);
};
Test Test::operator -(Test & p)
{
return this->a-p.a; //buf-buf1
}
void B::nihao(Test & p)
{
cout <<p.other_room<<endl;
}
int main()
{
Test buf(10);
Test buf1(5);
cout <<(buf-buf1).a<<endl;
return 0;
}
4.2.4.特殊运算符重载
4.2.4.1 赋值运算符重载
格式 : 返回值类型 operator =(形参)
除了之前学习的无参构造函数、拷贝构造函数、与析构函数以外,如果程序不手写,编译器还会给一个类添加赋值运算符重载函数。
赋值运算符重载只能使用成员函数运算符重载实现。
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
public:
MyInt(int a):a(a){}
int get_int()
{
return a;
}
// 编译器会自动添加赋值运算符重载函数
MyInt & operator =(MyInt &i)
{
cout << "赋值运算符重载函数被调用了" << endl;
this->a = i.a;
return *this;
}
};
int main()
{
MyInt int1(2);
MyInt int2(5);
cout << int1.get_int() << endl; // 2
cout << int2.get_int() << endl; // 5
MyInt int3 = int2 = int1;
MyInt int4 = int2; // 拷贝构造函数(隐式调用)
int2 = int4; // 赋值运算符重载
cout << int1.get_int() << endl; // 2
cout << int2.get_int() << endl; // 2
cout << int3.get_int() << endl; // 2
return 0;
}
当类中出现指针成员时,默认的赋值运算符重载函数类似于默认的浅拷贝构造函数,因此也需要手动编写解决“浅拷贝”的问题。
【面试题】一个类什么都不写,编译器帮你添加了那些函数?
无参构造函数、析构函数、拷贝构造函数、赋值运算符重载函数
【面试题】一个类中不写任何的权限,默认权限是什么?
私有权限
4.2.4.2 类型转换运算符重载
类型转换运算符重载是 C++ 中的一种特殊的运算符重载,它允许你将一个类的对象转换为另一种类型。通过类型转换运算符重载,可以方便地将自定义类型的对象转换为内置类型或其他自定义类型。
格式:operator 数据类型()
必须使用成员函数运算符重载,且格式比较特殊
#include <iostream>
using namespace std;
class MyInt
{
private:
int a1;
string st = "hello";
public:
MyInt(int a1):a1(a1){}
int get_int()
{
return a1;
}
// 编译器会自动添加赋值运算符重载函数
MyInt & operator =(MyInt &i)
{
cout << "赋值运算符重载函数被调用了" << endl;
this->a1 = i.a;
return *this;
}
// 类型转换运算符重载
operator int()
{
return a1;
}
operator string()
{
return st;
}
};
int main()
{
MyInt int1(2);
int a = int1;
cout << a << endl; // 2
string str = int1;
cout << str << endl; // hello
return 0;
}
4.2.5. 注意事项
- 重载的运算符限制在C++语言中已有的运算符范围,不能创建新的运算符。
- 运算符重载本质上也是函数重载,但是不支持函数参数默认值设定。
- 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符的操作数和语法结构。
- 运算符重载必须基于或者包含自定义类型,即不能改变基本数据类型的运算规则。
- 重载的功能应该与原有功能相似,避免滥用运算符重载。
- 一般情况下,双目运算符建议使用友元函数运算符重载,单目运算符建议使用成员函数运算符重载。
4.3.std::string 字符串类(熟悉)
字符串对象是一种特殊类型的容器,准们设计用于操作字符串。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string s; // 创建一个空字符串。
// 判断是否为空
cout << s.empty() << endl; // 1
// 隐式调用构造函数
string s1 = "hello";
cout << s1 << endl; // hello
// 显示调用
string s2("world");
cout << s2 << endl; // world
// ==、!=、> < 都是判断的编码
cout << (s1 == s2) << endl; // 0
cout << (s1 != s2) << endl; // 1
cout << (s1 > s2) << endl; // 0
cout << (s1 < s2) << endl; // 1
// 拷贝构造
string s3(s2); // 等同于 stirng s3 = s2;
cout << s3 << endl; // world
// 参数1:char* 源字符串
// 参数2:保留的字符数
string s4("ABCDEFG",3);
cout << s4 << endl; // ABC
// 参数1:std::string 原字符串
// 参数2:不保留的字符数,从头开始
string s5(s2,3);
cout << s5 << endl; // ld
// 参数1:字符数量
// 参数2:字符内容char
string s6(5,'a');
cout << s6 << endl; // aaaaa
// 交换
cout << "原s5 = " << s5 << " " << "原s6 = " << s6 << endl;
swap(s5,s6);
cout << "s5 = " << s5 << " " << "s6 = " << s6 << endl;
// 字符串拼接
string s7 = s5 + s6;
cout << s7 << endl; // aaaaald
// 向后追加字符串
s7.append("jiajia");
cout << s7 << endl; // aaaaaldjiajia
// 向后追加单字符
s7.push_back('s');
cout << s7 << endl; // aaaaaldjiajias
// 插入
// 参数1:插入的位置
// 参数2:插入的内容
s7.insert(1,"234");
cout << s7 << endl; // a234aaaaldjiajias
// 删除
// 参数1:起始位置
// 参数2:删除的字符数量
s7.erase(2,5);
cout << s7 << endl; // a2aldjiajias
// 替换
// 参数1:起始位置
// 参数2:被替换的字符数
// 参数3:替换的新内容
s7.replace(0,3,"***");
cout << s7 << endl; // ***ldjiajias
// 清空
s7.clear();
cout << s7.length() << endl; // 0
string s8 = "hahaha";
cout << s8 << endl; // hahaha
// 重新赋值
s8 = "ABCDEFGH";
cout << s8 << endl; // ABCDEFGH
// 参数1:拷贝的目标
// 参数2:拷贝的字符数量
// 参数3:拷贝的起始位置
// C++的string到C的string也就是数组
char arr[20] = {0};
s8.copy(arr,6,1);
cout << arr << endl; // BCDEFG
// C++ string到C string用到了C语言中的strcpy
// c_str C++的字符串转换成C语言的字符数组
// c_str 返回一个const char *
char c[20] = {0};
strcpy(c,s8.c_str());
cout << c << endl; // ABCDEFGH
return 0;
}
五、模板与容器
5.1.模板(掌握)
模板可以让类或者函数支持一种通用类型,这种通用类型在实际运行过程中可以使用任何数据类型,因此程序员写出一些与类型无关的代码,这种编程方式也被称为泛型编程。
通常有两种形式:
- 函数模板
- 类模板
注意:
模板实例化是在编译时进行的
模板的类型参数可以有默认值
5.1.1 函数模板
格式:template<class 类模板名> //class也可以替换成typename
调用格式:类模板名 函数名(类模板名1 形参名1,类模板名2 形参名2........)
注意:根据需要来。
使一个函数支持模板编程,可以使函数支持通用数据类型。
#include <iostream>
using namespace std;
template<class T> // 可以是class也可以是typename
T add(T a,T b)
{
return a + b;
}
int main()
{
string s1 = "hello";
string s2 = "world";
cout << add(1,1) << endl;
return 0;
}
5.1.2 类模板
格式:template<class 类模板名> //class也可以替换成typename
调用格式:类名<数据类型>类名
使一个类支持模板编程,可以使一个类支持通用数据类型。
#include <iostream>
using namespace std;
template<class T> // 可以是class也可以是typename
T add(T a,T b)
{
return a + b;
}
template<typename T>
class Test
{
private:
T val;
public:
Test(T v):val(v){}
T get_val()const
{
return val;
}
void set_val(const T &val)
{
this->val = val;
}
};
int main()
{
Test<int> t1(10);
cout << t1.get_val() << endl; // 10
Test<double> t2(20.3);
cout << t2.get_val() << endl; // 20.3
Test<string> t3("hello");
cout << t3.get_val() << endl; // hello
return 0;
}
如何将上述代码,改为类内声明,类外实现?
#include <iostream>
using namespace std;
template<class T> // 可以是class也可以是typename
T add(T a,T b)
{
return a + b;
}
template<class T>
class Test
{
private:
T val;
public:
Test(T v);
T get_val()const;
void set_val(const T &val);
};
template<class T>
Test<T>::Test(T v):val(v)
{
}
template<class T>
T Test<T>::get_val()const
{
return val;
}
template<class T>
void Test<T>::set_val(const T &val)
{
this->val = val;
}
int main()
{
Test<int> t1(10);
cout << t1.get_val() << endl; // 10
Test<double> t2(20.3);
cout << t2.get_val() << endl; // 20.3
Test<string> t3("hello");
cout << t3.get_val() << endl; // hello
return 0;
}
5.2.容器
5.2.1 标准模板库STL
标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。虽说它主要出现到了C++中,但是在被引入C++之前该技术就已经存在了很长时间。
STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
5.2.2 概念
容器是用来存储数据的集合,数据元素可以是任何类型(因为是使用模板实现)。
容器类的使用,都要引入对应的头文件。
5.2.3 顺序容器
顺序容器中每个元素都有固定的位置并呈线性排布,除非使用删除或者插入操作改变元素位置。
5.2.3.1 array数组
格式:array<数据类型,长度>数组名 ={};
array是C++11新增的容器类型,与传统数组相比更加的安全、易于使用。array数组是定长的,没办法方便的伸缩。
#include <iostream>
#include <array>
using namespace std;
int main()
{
// 创建一个长度为5的int数组
array<int,5> arr = {1,2,3}; // 后两位补零
cout << arr[1] << endl;
cout << arr[4] << endl;
cout << arr.at(2) << endl; // 3 推荐使用
arr[3] = 200;
// for
for(int i = 0; i < arr.size(); i++)
{
cout << arr.at(i) << endl;
}
cout << "-----------------" << endl;
// for each
for(int i : arr)
{
cout << i << endl;
}
// 迭代器遍历,后边讲
return 0;
}
5.2.3.2 vector向量
格式:vector<数据类型>向量名={元素};
格式2:vector<数据类型> 向量名(长度);
vector内部是由数组实现的。比较适合进行随机的存取操作。但是不擅长插入和删除操作。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// vector<int> v = {1,2,3};
// for(int i : v)
// {
// cout << i << endl;
// }
// 创建一个长度为5的int向量
vector<int> vec(5);
// cout << vec.size() << endl; // 5
// 增
vec.push_back(222);
// cout << vec.size() << endl; // 6
// 插入操作
vec.insert(vec.begin()+2,333); // begin可以返回指向第一个元素的迭代器指针,+2是在第三个位置上插入333
// 改
vec[0] = 1;
vec[1] = 2;
vec[3] = 4;
vec[5] = 6;
// 删
// 删除最后一个元素
vec.pop_back();
// 删除第二个元素
vec.erase(vec.begin()+1);
for(int i : vec)
{
cout << i << " ";
}
return 0;
}
//向后追加
buf1.push_back(22);
//插入操作
buf1.insert(buf1.begin()+1,33);
for(int i1:buf1)
{
cout <<i1;
}
//改
for(int i1=0;i1<buf1.size();++i1)
{
buf1.at(i1)=i1;
}
//删除最后一个 元素
buf1.pop_back();
//删除第二个元素
buf1.erase(buf1.begin()+1);
//删除倒数第二个元素
buf1.erase(buf1.end()-2);
5.2.3.3 list 列表
list内部是由双向链表实现,内部空间不连续,不支持下标。优势:可以高效的删除和插入操作。但是不适合随机存取操作。
#include <iostream>
#include <list>
using namespace std;
int main()
{
// 创建一个默认无数值的list
list<string> lis1;
// 创建一个长度为2的列表,第一个元素是hello,第二个元素是world
// list<string> lis2{"hello","world"};
// for(string s : lis2)
// {
// cout << s << endl;
// }
// 创建一个长度为5的列表,每个元素都是“hello”
list<string> lis(5,"hello");
// 增
lis.push_back("world"); // 向后追加单元素
lis.push_front("hahaha"); // 向前追加单元素
lis.insert(++lis.begin(),"222"); // 在第二个位置上插入“222”
// 删
// lis.pop_back(); // 删除最后一个元素
// lis.pop_front(); // 删除第一个元素
// 迭代器指针
list<string>::iterator iter = lis.begin();
advance(iter,1); // 移动迭代器
lis.insert(iter,"333"); // 插入333
// 删除最后一个元素
// iter = lis.end();
// iter--;
// lis.erase(iter);
// 删除的首元素
iter = lis.end();
iter++;
lis.erase(iter);
// 不要删除end
// iter = lis.end();
// lis.erase(iter);
iter = lis.begin();
advance(iter,1);
lis.erase(iter);
// 返回第一个元素的引用
// cout << lis.front() << endl;
// 返回最后一个元素的引用
// cout << lis.back() << endl;
// 改
iter = lis.end();
advance(iter,2);
*iter = "200";
// 查
cout << " ---- " << *iter << endl;
// 遍历
for(string s : lis)
{
cout << s << endl;
}
// 不能使用普通循环遍历,因为list不支持下标。
// QList 是支持下标访问的。
// 迭代器遍历,后面讲
// 清空
lis.clear();
cout << lis.size() << endl;
return 0;
}
5.2.3.4 deque队列
格式:deque<数据类型> 队列名={与数据类型对应的参数1,与数据类型对应的参数2,..........};
格式2:deque<数据类型> 队列名(长度);
deque几乎支持所有vector的API,性能位于vector与list两者之间,最擅长两端存储的顺序容器。
#include <iostream>
#include <deque>
using namespace std;
int main()
{
// deque<int> v = {1,2,3};
// for(int i : v)
// {
// cout << i << endl;
// }
// 创建一个长度为5的int向量
deque<int> deq(5);
// cout << deq.size() << endl; // 5
// 增
deq.push_back(222);
// cout << deq.size() << endl; // 6
// 插入操作
deq.insert(deq.begin()+2,333); // begin可以返回指向第一个元素的迭代器指针,+2是在第三个位置上插入333
// 改
deq[0] = 1;
deq[1] = 2;
deq[3] = 4;
deq[5] = 6;
deq.at(0) = 32;
// 删
// 删除最后一个元素
deq.pop_back();
// 删除第二个元素
deq.erase(deq.begin()+1);
// 删除倒数第二个元素
deq.erase(deq.end()-2);
// 查询
cout << deq[0] << endl;
cout << deq.at(1) << endl;
// 循环
for(int i : deq)
{
cout << i << " ";
}
cout << endl;
for(int i = 0; i < deq.size(); ++i)
{
cout << deq.at(i) << endl;
}
// 判断否为空,0非空,1空
cout << deq.empty() << endl;
// 清空
deq.clear();
cout << deq.empty() << endl;
// 迭代器遍历,先省略
return 0;
}
5.2.4 关联容器
格式1:map <数据类型1,数据类型2>关联容器名;
格式2(赋初始值):map <数据类型1,数据类型2>关联容器名={{与数据类型1对应的参数,与数据类型2对应的参数},{与数据类型1对应的参数,与数据类型2对应的参数},{与数据类型1对应的参数,与数据类型2对应的参数}}
关联容器的各个元素之间没有严格顺序,虽然内部具有升序的特点(基于红黑树实现,元素按照键的升序进行存储),但是在使用时没有任何顺序相关接口。所以遍历时,是按照内部排序进行输出。
最常见的关联容器就是map-键值对映射。
对于map而言,键是唯一的,具有唯一性,键通常使用字符串类型,值可以是任何类型。通过这个唯一的键,可以找到对应的值。
#include <iostream>
#include <map>
using namespace std;
int main()
{
// 列表初始化,C++11支持
map<string,int> ma1 = {{"年龄",18},{"体重",189}};
cout << ma1.size() << endl; // 2个元素
// 创建一个元素为0的键值对对象
map<string,int> ma;
cout << ma.size() << endl; // 0
// 增
ma["身高"] = 180; // 插入元素
cout << ma.size() << endl; // 1
ma.insert(pair<string,int>("体重",189)); // 插入元素
cout << ma.size() << endl; // 2
// 改
ma["身高"] = 175;
ma["体重"] = 999;
// 查
cout << ma["身高"] << endl; // 175
cout << ma["体重"] << endl; // 999
if(ma.find("身高") == ma.end()) // frnd从头开始查询,如果没有找到就会返回end
{
cout << "没有身高元素" << endl;
}
else
{
cout << ma["身高"] << endl;
}
// 支持for each循环
for(pair<string,int>i : ma)
{
// first表示键,second表示值
cout << i.first << " " << i.second << endl;
}
// 删除、删除之前可以判断元素是否存在
int re = ma.erase("身高"); // 删除返回值:1表示成功,0表示失败
cout << "身高删除:" << re << endl;
re = ma.erase("年龄");
cout << "身高删除:" << re << endl; // 0
cout << ma.size() << endl; // 1
// 清空
ma.clear();
cout << ma.size() << endl;
return 0;
}
5.2.5迭代器
迭代器是一个特殊的指针,主要用于容器的元素的读写以及遍历。
格式:容器类型<数据类型>::const_iterator 迭代器名字=地址(begin())
const_iterator 与iterator的区别
iterator
:普通迭代器,它提供了读写访问容器元素的能力。可以使用它来遍历容器中的元素,并且可以修改所指向的元素的值。const_iterator
:常量迭代器,它只提供对容器元素的只读访问。使用const_iterator
可以遍历容器,但不能修改所指向的元素的值,主要用于在不希望修改容器内容的场景下进行遍历操作。
#include <iostream>
#include <map>
#include <array>
#include <vector>
#include <list>
#include <deque>
using namespace std;
int main()
{
string s = "abcdefg";
for(string::const_iterator iter = s.begin();iter != s.end(); iter++)
{
cout << *iter << " " ;
}
cout << endl;
cout << "-------------------" << endl;
// 迭代器遍历array
array<int,5> arr = {21,3,4,65,2};
for(array<int,5>::const_iterator iter = arr.begin(); iter != arr.end();iter++)
{
cout << *iter << endl;
}
cout << "-------------------" << endl;
// 迭代器遍历vector
vector<string> vec(6,"hello");
for(vector<string>::iterator iter = vec.begin(); iter != vec.end(); iter++)
{
cout << *iter << endl;
}
cout << "-------------------" << endl;
// 迭代器遍历list
list<string> lis(6,"world");
for(list<string>::const_iterator iter = lis.begin(); iter != lis.end();iter++)
{
cout << *iter << endl;
}
cout << "-------------------" << endl;
// 迭代器遍历deque
deque<string> de(6,"hahha");
for(deque<string>::const_iterator iter = de.begin();iter != de.end();iter++)
{
cout << *iter << endl;
}
cout << "-------------------" << endl;
// 迭代器遍历map
map<string,int> ma = {{"年龄",12},{"身高",145},{"体重",60}};
for(map<string,int>::const_iterator iter = ma.begin();iter != ma.end();iter++)
{
cout << iter->first << " " << iter->second << endl;
}
return 0;
}
5.2.5.1.find函数
std::find
是 C++ 标准库 <algorithm>
头文件中提供的一个通用算法,用于在指定的范围内查找某个值。
函数原型
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int target = 3;
auto it = std::find(numbers.begin(), numbers.end(), target);
if (it != numbers.end()) {
std::cout << "Found " << target << " at position " << std::distance(numbers.begin(), it) << std::endl;
} else {
std::cout << target << " not found." << std::endl;
}
return 0;
}
参数说明
-
first
:指向要查找范围起始位置的迭代器。 -
last
:指向要查找范围结束位置的下一个位置的迭代器。 -
value
:要查找的值。
返回值
如果在 [first, last)
范围内找到与 value
相等的元素,则返回指向该元素的迭代器;如果未找到,则返回 last
。
6.面向对象核心
6.1.继承
6.1.1.概念
继承是面向对象三大特征之一,体现了代码复用的思想。
继承就是在一个已存在的类的基础上建立了一个新的类,并拥有其特性。
- 已存在的类被称为“基类”,或者“父类”。
- 新建立的类被称为“派生类”或者“子类”。
格式:class 派生类名 :public 基类名
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是厨师,我负责炒菜" << endl;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
};
int main()
{
Son s;
cout << s.get_name() << endl;
s.work();
return 0;
}
上面的代码,Son类的功能几乎与Father类重叠,在实际的使用过程中,派生类往往会做出一些与基类的差异化。
- 直接修改继承来的基类内容。
属性:1、公有的属性可以直接更改。私有的属性,需要使用基类公有函数进行修改。
行为:函数,函数隐藏。通过派生类实现一个同名的函数,来隐藏基类的函数。
- 新增派生类的内容
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
int age = 12;
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是厨师,我负责炒菜" << endl;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
private:
int a = 10;
public:
void init()
{
age = 11;
set_name("王");
}
void work()
{
cout << "我的工作是程序猿,我负责使用挖掘机来炒菜" << endl;
}
void game()
{
cout << "我不光干活,我还玩游戏,原神启动" << endl;
}
};
int main()
{
Son s;
cout << s.get_name() << endl; // 孙
s.work(); // 我的工作是程序猿,我负责使用挖掘机来炒菜
cout << s.age << endl; // 12
s.init();
cout << s.get_name() << endl; // 王
cout << s.age << endl; // 11
s.game(); // 我不光干活,我还玩游戏,原神启动
// 调用基类被隐藏的成员函数
s.Father::work(); // 我的工作是厨师,我负责炒菜
return 0;
}
基类和派生类是相对的,一个类可能存在即是基类有是派生类的情况,取决于那两个类进行比较。
6.2.构造函数
6.2.1.派生类与基类构造函数的关系
构造函数和析构函数不能被继承。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father(string name):name(name){}
string get_name()
{
return name;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
public:
Son()
{
}
};
int main()
{
Son s; // 找不到基类的无参构造函数
Son s("张"); // 没有匹配的构造函数
return 0;
}
创建对象必须要调用任意一个构造函数。
6.2.1.解决方案
6.2.1.1 补充基类的无参构造函数
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father(string name):name(name){}
Father(){}
string get_name()
{
return name;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
public:
Son()
{
}
};
int main()
{
Son s; // 找不到基类的无参构造函数
// Son s("张"); // 没有匹配的构造函数
return 0;
}
6.2.1.3 手动在派生类中调用基类的构造函数
6.2.1.3.1 透传构造
格式:派生类构造函数 :基类构造函数(参数){}
在派生类的构造函数中,调用基类的构造函数,实际上编译器自动添加派生类的构造函数,调用基类无参构造函数时,就采用的这种方式。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father(string name):name(name){}
Father(){}
string get_name()
{
return name;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
public:
// 编译器会自动添加构造函数(透传构造)
// Son():Father(){}
Son():Father("王"){}
Son(string fn):Father(fn){}
};
int main()
{
Son s;
cout << s.get_name() << endl;
Son s1("张");
cout << s1.get_name() << endl;
return 0;
}
1.2.2.2.2 委托构造
一个类的构造函数可以调用这个类的另一个构造函数,但是要避免循环委托。
委托构造的性能低于透传构造,但是代码的维护性会”更好“。因为通常一个类中构造函数都会委托给能力最强(参数最多)的构造函数。代码重构时,只需要更改这个能力最强的构造函数即可。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father(string name):name(name){}
string get_name()
{
return name;
}
};
// 派生类Son继承基类Father
class Son:public Father
{
public:
// 委托构造
Son():Son("王")
{
cout << "无参构造函数" << endl;
}
Son(string fn):Father(fn)
{
cout << "有参构造函数" << endl;
}
};
int main()
{
Son s;
cout << s.get_name() << endl;
// Son s1("张");
// cout << s1.get_name() << endl;
return 0;
}
1.2.2.2.3 继承构造
格式:using 基类类名::基类类名;
C++11新增的写法,只需要一句话,就可以给派生类添加n个构造函数(n为基类构造函数的个数)。并且每一个派生类的构造函数格式都与基类相同。每个派生类的构造函数都通过透传构造调用对应格式的基类构造函数。
-
#include <iostream> using namespace std; // 基类 class Father { private: string name = "孙"; public: Father():Father("张"){} // 委托构造 Father(string name):name(name){} string get_name() { return name; } }; // 派生类Son继承基类Father class Son:public Father { public: // 只需要加一句话,编译器就会自动添加下面两种构造函数 using Father::Father; // 委托构造 // Son():Father() // { // cout << "无参构造函数" << endl; // } // Son(string fn):Father(fn) // { // cout << "有参构造函数" << endl; // } }; int main() { // Son s; // cout << s.get_name() << endl; Son s1("王"); cout << s1.get_name() << endl; return 0; }
6.3.对象的创建与销毁流程
在继承中,构造函数与析构函数的调用顺序
#include <iostream>
using namespace std;
class Value
{
private:
string str;
public:
Value(string str):str(str)
{
cout << str << "构造函数" << endl;
}
~Value()
{
cout << str << "析构函数" << endl;
}
};
class Father
{
public:
static Value s_value;
Value val = Value("Father成员变量");
Father()
{
cout << "Father 构造函数被调用了" << endl;
}
~Father()
{
cout << "Father 析构函数被调用了" << endl;
}
};
Value Father::s_value = Value("静态FatherValue");
class Son : public Father
{
public:
static Value s_value;
Value val = Value("Son成员变量");
Son()
{
cout << "Son 构造函数被调用了" << endl;
}
~Son()
{
cout << "Son 析构函数被调用了" << endl;
}
};
Value Son::s_value = Value("静态SonValue");
int main()
{
cout << "主函数被调用了" << endl;
// 局部代码块
{
Son s;
cout << "对象执行中" << endl;
}
cout << "主函数结束了" << endl;
return 0;
}
上面的执行结果中,可以得到以下的规律:
- 静态的创建早于非静态
- 在创建的过程中,同类型的内存空间区域基类先开辟,派生类先销毁。
- 以“对象执行中”为轴,上下对称。
6.4 多重继承
6.4.1 概念
C++支持多重继承,即一个派生类可以有多个基类。派生类对于每个基类的关系仍然可以看做成一个单继承。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "沙发可以坐着" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "床可以躺着" << endl;
}
};
class SofaBed : public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
6.4.2 可能出现的问题
6.4.2.1 问题1 - 重名问题
当多个基类具有重名成员时,编译器在编译的过程中会出现二义性问题。
解决方法:使用基类的类名::方式调用。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "沙发可以坐着" << endl;
}
void clean()
{
cout << "打扫沙发" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "床可以躺着" << endl;
}
void clean()
{
cout << "打扫床" << endl;
}
};
class SofaBed : public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
sb.Sofa::clean();
sb.Bed::clean();
return 0;
}
6.4.2.2 问题2-菱形继承(熟悉)
缺点:
- 可维护性和可扩展性:由于继承关系变得复杂,代码的可维护性会下降
- 菱形继承和虚继承增加了代码的复杂性,使类的继承关系变得难以理解和维护。
- 由于虚继承引入了虚基类表和虚基类指针,当访问虚基类的成员时,需要通过额外的指针间接访问,这会导致性能开销。
当一个派生类有多个基类,且这些基类又有一个共同的基类时,就会出现二义性问题。这种现象也被称为菱形(钻石)继承。
有两种解决方式:
- 使用基类::方式调用
#include <iostream>
using namespace std;
// 家具厂
class Furniture
{
public:
void func()
{
cout << "家具厂里有家驹" << endl;
}
};
class Sofa:public Furniture
{
public:
};
class Bed:public Furniture
{
public:
};
class SofaBed : public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.Sofa::func();
sb.Bed::func();
return 0;
}
2、使用虚继承(熟悉)
在编译的时候有虚继承时,Furniture类中会生成一张虚基类表,这个表不占用任何对象的存储空间,数据Furniture类持有,在程序启动时加载进内存空间,表中记录了Furniture函数的调用地址偏移量。
Bed和Sofa对象中会出现一个隐藏的成员变量指针,指向Furniture类中的虚基类表,占用对象四个字节。
虚继承时,SofaBed类对象会同时拥有两个虚基类表指针成员,在调用时查表解决二义性问题。
#include <iostream>
using namespace std;
// 家具厂
class Furniture
{
public:
void func()
{
cout << "家具厂里有家驹" << endl;
}
};
// 虚继承
class Sofa : virtual public Furniture
{
public:
};
class Bed : virtual public Furniture
{
public:
};
class SofaBed : public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.func();
return 0;
}
6.5.权限
6.5.1 权限修饰符
三种权限一共对应九种场景。要做到心中有表,遇到任何一种场景都能直接反映出来是否能够访问。
类内 | 派生类中 | 全局 | |
private | √ | × | × |
protected | √ | √ | × |
public | √ | √ | √ |
#include <iostream>
using namespace std;
class Base
{
protected:
string s = "保护权限";
public:
Base()
{
cout << s << endl;
}
};
class Son:public Base
{
public:
Son()
{
cout << s << endl;
}
};
int main()
{
Son s;
// cout << s.s << endl; // 错误保护权限,无法在类外使用
return 0;
}
6.5.2 不同权限的继承
6.5.2.1 公有继承
在上面的代码中一直使用的就是公有继承,公有继承也是使用最多的一种继承方式。
在公有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员和保护成员在派生类中权限不变。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:public Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有权限
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s;
// cout << s.str1 << endl; // 错误 str1为私有权限
// cout << s.str2 << endl;
cout << s.str3 << endl;
return 0;
}
6.5.2.2 保护继承
在保护继承中,派生类可以继承基类的成员,不可以访问基类的私有成员,基类的公有成员和保护成员在派生类中的权限都是保护权限。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:protected Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有权限
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s;
// cout << s.str1 << endl; // 错误 str1为私有权限
// cout << s.str2 << endl; // 错误
// cout << s.str3 << endl; // 错误
return 0;
}
6.5.2.3 私有继承
在私有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有继承成员和保护成员在派生类中的权限都是私有权限。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:private Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有权限
cout << str2 << endl;
cout << str3 << endl;
}
};
class SonSon:public Son
{
public:
SonSon()
{
// cout << str1 << endl; // 错误 str1为私有权限
// cout << str2 << endl; // 错误
// cout << str3 << endl; // 错误
}
};
int main()
{
SonSon s;
// cout << s.str1 << endl; // 错误 str1为私有权限
// cout << s.str2 << endl; // 错误
// cout << s.str3 << endl; // 错误
return 0;
}
6.6.、多态(重点)
6.6.1 什么是多态
在面向对象编程中,我们通常将多态分为两种类型:静态多态(也被称为编译时多态)、动态多态(被称为运行时多态)。
静态多态:
- 静态多态是指在编译时就能够确定要调用的函数,通过函数重载和运算符重载实现。
动态多态:
- 动态多态是指,在运行时根据对象的实际类型,来确定要调用的函数,通过继承和函数覆盖来实现。
静态多态发生在编译时。
动态多态发生在运行时。具体调用那个函数是在程序运行时根据对象的实际类型确定的。
优点和缺点:
- 可以在基类中定义通用的接口,在派生类中实现具体的行为,减少代码的重复。
- 可以方便地添加新的派生类,而无需修改使用基类指针或引用的代码。
- 于多态性,代码的结构更加清晰,基类定义接口,派生类实现具体功能,易于理解和维护。
6.6.2 多态的概念
多态可以理解为”一种接口,多种状态“,只需要编写一个函数接口,根据传入的参数类型执行不同的策略代码。
多态的实现具有三个前提条件:
- 公有继承
- 函数覆盖
- 基类引用/指针指向派生类对象。
6.6.3 函数覆盖
格式:函数与基类函数的返回值,函数名、形参相同,可以用const修饰,但不能用static修饰
函数覆盖、函数隐藏。但是函数隐藏不支持多态,而函数覆盖是多态的必要条件。
有以下区别:
- 函数隐藏是派生类中存在与基类同名的函数,编译会将基类的同名函数进行隐藏。
- 函数覆盖是基类中定义了一个虚函数,派生类编写一个同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的基类函数必须是虚函数。
6.6.4 虚函数的定义
格式:virtual 函数类型 函数名()
一个函数使用virtual关键字修饰,就是虚函数,虚函数是函数覆盖的前提。在Qt Creator中虚函数的函数名称使用斜体字。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
int main()
{
return 0;
}
虚函数具有以下性质:
- 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog : public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
int main()
{
return 0;
}
- 只有普通成员函数与析构函数可以声明为虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 错误,构造函数不能声明为虚函数
// virtual Animal()
// {
// }
// 静态成员函数不能声明为虚函数
// virtual static void testStatic()
// {
// }
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog : public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
int main()
{
return 0;
}
- 在C++11中,可以在派生类新覆盖的函数上使用override关键字验证覆盖是否成功。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
void test()
{
}
};
class Dog : public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
// 错误 这是隐藏,不是覆盖,override验证函数是否成功,覆盖失败会直接导致报错
// void test() override
// {
// }
};
int main()
{
return 0;
}
6.6.5 多态实现
要实现动态多态,需要有三个前提条件:
- 公有继承
- 函数覆盖
- 基类指针或引用 指向派生类对象
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog : public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "猫爱吃鱼" << endl;
}
};
void animal_eat(Animal *a1)
{
// 100 代码
a1->eat();
}
void animal_eat2(Animal &a1)
{
// 100 代码
a1.eat();
}
int main()
{
Dog *d1 = new Dog;
Cat *c1 = new Cat;
animal_eat(d1);
animal_eat(c1);
Dog d2;
Cat c2;
animal_eat2(d2);
animal_eat2(c2);
return 0;
}
6.6.6 多态原理
具有虚函数的类会存在一张虚函数表,每个类的对象内部,都会有一个隐藏的虚函数表指针成员,指向当前类的虚函数表。
多态实现流程:
在代码运行时,通过对象的虚函数表指针找到对应虚函数表,在表用定位虚函数的调用地址,从而执行相应的虚函数内容。
6.6.7 虚析构函数
如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,只能出发基类的析构函数,如果在派生类中申请内存等资源,则会导致内存空间无法释放,出现内存泄漏的问题。
解决方案是给基类的析构函数使用virtual修饰为虚析构函数,通过传递性可以把各个派生类析构函数都变为虚析构函数,因此建议给一个可能为基类的类中的析构函数设置成虚析构函数。
#include <iostream>
using namespace std;
class Animal
{
public:
void eat()
{
cout << "动物爱吃饭" << endl;
}
virtual ~Animal()
{
cout << "基类析构函数调用了" << endl;
}
};
class Dog : public Animal
{
public:
void eat()
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "派生类析构函数调用了" << endl;
}
};
int main()
{
// 派生类析构函数没有调用
// Animal *a1 = new Dog;
// delete a1;
Animal *a1 = new Dog;
delete a1;
return 0;
}
6.6.8 类型转换
在上一节中除了虚析构函数外,还可以使用类型转换解决内存泄漏的问题,以下是传统的类型转换写法。
#include <iostream>
using namespace std;
class Animal
{
public:
void eat()
{
cout << "动物爱吃饭" << endl;
}
~Animal()
{
cout << "基类析构函数调用了" << endl;
}
};
class Dog : public Animal
{
public:
void eat()
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "派生类析构函数调用了" << endl;
}
};
int main()
{
Animal *a1 = new Dog;
// 把 a1 转换成Dog *类型
Dog* d = (Dog *)a1;
delete d;
return 0;
}
在C++11中不建议使用以上C风格的类型转换,因为可能会带来一些安全隐患,让程序难以发现。
C++11提供了一组适用于不同场景的强制类型转换函数。
- static_cast(静态转换)
- dynamic_cast(动态转换)
- const_cast(常量转换)
- reinterpret_cast(重解释转换)
6.6.8.1 static_cast
- 主要用于基本数据类型之间的转换
格式:数据类型 变量名 =static_cast<数据类型>(变量)
#include <iostream>
using namespace std;
int main()
{
int x = 1;
double y = static_cast<double>(x);
cout << y << endl;
return 0;
}
static_cast没有运行时类型检查来保证转换的安全性,需要程序员手动判断转换是否安全。
static_cast 也可以用于类层次的转换中,即基类和派生类指针或者引用之间的转换。
- static_cast进行上行转换是安全的,即把派生类的指针或者引用转换为基类的。
- static_cast进行下行转换是不安全的,即把基类的指针或者引用转换为派生类的。
#include <iostream>
using namespace std;
class Father
{
public:
string a = "Father";
};
class Son:public Father
{
public:
string b = "Son";
};
int main()
{
// 上行转换
Son *s1 = new Son;
Father *f1 = static_cast<Father*>(s1);
cout << f1->a << endl; // Father
// 下行转换
Father *f2 = new Father;
Son *s2 = static_cast<Son*>(f2);
cout << s2->a << endl; // Father
cout << s2->b << endl; // 结果不定
return 0;
}
static_cast 和C语言的强制类型转换相比:
- static_cast的表达式更清晰,方便管理
- static_cast会在编译的时候进行类型检擦。
static_cast 也可以转换自定义类型,但是目标类型必须含有对应参数的构造函数。
#include <iostream>
using namespace std;
class Student
{
private:
string name;
public:
Student(string name):name(name){}
string get_name()const
{
return name;
}
};
int main()
{
Student st("张三");
Student s = static_cast<Student>("Tom");
cout << s.get_name() << endl;
return 0;
}
6.6.8.2 dynamic_cast
格式:派生类名* 指针名= dynamic_cast<派生类名*>(基类指针)
格式2:派生类名& 引用名= dynamic_cast<派生类名&>(基类引用)
dynamic_cast主要用于类层次之间的上行或者下行转换。
在进行上行转换时,dynamic_cast与static_cast效果相同,但是在进行下行转换时,dynamic_cast会比static_cast更加安全。
关于下行转换类型检查的逻辑如下:
#include <iostream>
using namespace std;
class Father
{
public:
virtual void func()
{
cout << "Father" << endl;
}
};
class Son:public Father
{
public:
void func()
{
cout << "Son" << endl;
}
};
int main()
{
// 基类指针指向派生类对象(形成多态)
Father *f0 = new Son;
Son *s0 = dynamic_cast<Son*>(f0);
f0->func(); // Son
s0->func(); // Son
// 指针未形成多态
Father *f1 = new Father;
Son *s1 = dynamic_cast<Son*>(f1);
f1->func(); // Father
// s1->func(); // 非法调用
// 引用且形成多态
Son s;
Father &f2 = s;
Son &s2 = dynamic_cast<Son &>(f2);
cout << &s2 << " " << &f2 << " " << &s << endl; // 0x61fe74 0x61fe74 0x61fe74
s2.func(); // Son
f2.func(); // Son
s.func(); // Son
// 引用且没形成多态
Father f;
Son &s3 = dynamic_cast<Son&>(f); // 运行终止
cout << &s3 << " " << &f << endl;
s3.func();
return 0;
}
6.6.8.3 const_cast
const_cast可以添加或者移除对象的const限定符。
格式:数据类型 变量名 =const_cast<数据类型>(变量名)
主要用于改变指针或者引用的const效果,以便于在一定的情况下修改原本被声明为常量的对象,应该避免使用const_cast,而是考虑通过设计良好的接口或者其他正常手段避免需要进行此类转换。
#include <iostream>
using namespace std;
class Test
{
public:
string str = "A";
};
int main()
{
const Test *t1 = new Test;
// t1->str = "B"; // 错误
Test *t2 = const_cast<Test *>(t1);
t2->str = "B";
// t1->str = "C"; // 错误
cout << t1 << " " << t2 << endl;
cout << t1->str << " " << t2->str << endl;
return 0;
}
6.6.8.4 reinterpret_cast
慎用!!
用于在不同类型的指针、引用之间进行转换,甚至可以将指针转换为整数类型,或者将整数类型转换为指针
#include <iostream>
int main() {
int num = 42;
int* intPtr = #
// 将 int* 转换为 char*
char* charPtr = reinterpret_cast<char*>(intPtr);
std::cout << "intPtr: " << intPtr << std::endl;
std::cout << "charPtr: " << static_cast<void*>(charPtr) << std::endl;
return 0;
}
#include <iostream>
using namespace std;
class A
{
public:
void print()
{
cout << "A" << endl;
}
};
class B
{
public:
void print()
{
cout << "B" << endl;
}
};
int main()
{
A *a = new A;
B *b = reinterpret_cast<B*>(a);
cout << a << " " << b << endl; // 0x7010f8 0x7010f8
a->print();
b->print();
return 0;
}
6.7. 抽象类
作用:指定算法框架
如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类。
如果一个类中有纯虚函数,则这个类一定是抽象类。
如果一个类是抽象类,则这个类中一定有纯虚函数。
纯虚函数是虚函数的一种。这种函数只有声明没有定义。
virtual 返回值类型 函数名称(参数列表) = 0;
不能直接使用抽象类作为声明类型,因为不存在抽象类类型的对象。(不能创建对象)
抽象类作为基类时,具有两种情况:
- 派生类继承抽象类,覆盖并实现基类所有的纯虚函数,此时派生类可以作为普通函数使用,即不再是抽象类。
- 派生类继承抽象类:没有把基类的所有的纯虚函数覆盖并实现,此时派生类也变为抽象类,等待他的派生类覆盖并实现剩余的纯虚函数。
#include <iostream>
using namespace std;
// 抽象类: 形状
class Shape
{
public:
// 纯虚函数
virtual void area() = 0; // 面积
virtual void perimeter() = 0; // 周长
};
// 圆形
class Circle : public Shape
{
public:
// 覆盖并实现基类所有的纯虚函数
void area()
{
cout << "圆形计算面积" << endl;
}
void perimeter()
{
cout << "圆形计算周长" << endl;
}
};
// 多边形
class polygon : public Shape
{
public:
void perimeter()
{
cout << "多边形计算周长" << endl;
}
};
// 矩形
class Rectangle : public polygon
{
public:
void area()
{
cout << "矩形计算面积" << endl;
}
};
int main()
{
// Shape p1; // 错误 抽象类无法实例化对象
Shape *p1; // 抽象类的指针/引用 可以存在
Circle c;
c.area();
c.perimeter();
// polygon pl; // 错误
Rectangle re;
re.area();
re.perimeter();
// 多态,抽象类支持多态
Shape *p2 = new Circle;
p2->area();
p2->perimeter();
return 0;
}
使用抽象类需要注意以下几点:
- 抽象类的析构函数必须是虚析构函数.
- 抽象类支持多态,可能存在指针、或者引用的声明格式
-
抽象类的 作用 是 指定 算法 框架 , 因此 在 一个 继承 体系 中 , 抽象类 的 内容 要 丰富 并且 重要
-
6.8.纯虚析构函数
-
纯虚析构函数的定义:
析构函数的作用,是销毁对象时回收资源的,而且析构函数不能被继承。
必须要为纯虚析构函数提供一个函数体。
纯虚析构函数,必须类内声明类外定义。
#include <iostream> using namespace std; // 抽象类 class Animal { public: // 纯虚析构函数 virtual ~Animal() = 0; }; // 纯虚析构函数类外定义 Animal::~Animal() { cout << "基类的纯虚析构函数被调用了" << endl; } class Dog:public Animal { public: ~Dog() { cout << "派生类析构函数被调用了" << endl; } }; int main() { // Animal a; // 错误 Animal有纯虚析构函数,所以是抽象类,无法实例化对象 Animal *a = new Dog; delete a; return 0; }
虚析构函数与纯虚析构函数的区别:
虚析构函数:virtual 关键字修饰,有函数体,不会导致类为抽象类。
纯虚析构函数:virtual关键字修饰,结果=0,函数体需要在类外实现,会导致类为抽象类。
7、异常处理(熟悉)
7.1.概念
异常时程序在执行期间产生的问题。C++异常指的是在程序运行时发生的特殊情况,比如下标范围越界等。
异常提供了一种转移控制权的方式。
程序一旦出现异常没有经过处理,就会造成程序运行崩溃。
处理异常的方式有两种:抛出异常(throw)和捕获异常(try-catch)
7.2.抛出异常
格式:throw 变量名
可以使用throw语句在代码块中任何位置抛出异常。
throw语句的操作数可以是任意的表达式,表达式的结果决定了抛出异常类型。
抛出的异常是抛出到函数调用的上一级。
#include <iostream> using namespace std; double division(double a,double b) { if(b == 0) { // 抛出一个异常 string text("除数等于0!"); throw text; // 抛出一个std::string } return a/b; } double input() { cout << "input 开始执行" << endl; double a; double b; cout << "请输入两个浮点型:" << endl; cin >> a >> b; double ret = division(a,b); // 1 text对象在这里 (无人处理) cout << "input执行结束" << endl; return ret; } int main() { cout << "程序开始执行" << endl; cout << input() << endl; // 2 text对象又被抛出到这里 (还是无人处理) cout << "程序执行结束" << endl; return 0; }
7.3.捕获异常
如果有一个try代码块抛出一个异常,捕获异常则使用catch代码块。
#include <iostream> using namespace std; double division(double a,double b) { if(b == 0) { // 抛出一个异常 string text("除数等于0!"); throw text; // 抛出一个std::string } return a/b; } double input() { cout << "input 开始执行" << endl; double a; double b; cout << "请输入两个浮点型:" << endl; cin >> a >> b; double ret; try // 尝试执行代码块的内容 { ret = division(a,b); } catch(string &e) // catch小扩号中要写抛出异常的类型(类型跟抛出的类型不符合,就会出现捕获不到的情况) { // 查看异常详细信息 cout << e << endl; // 补救措施 return 0; } cout << "input执行结束" << endl; return ret; } int main() { cout << "程序开始执行" << endl; cout << input() << endl; cout << "程序执行结束" << endl; return 0; }
上述的代码可能会出现以下几种情况:
- 无异常抛出,此时程序正常执行,不会进去catch块
- 异常抛出,正常捕获,此时程序进入catch块
- 异常抛出,错误捕获(捕获的类型不匹配),此时程序仍然会向上抛出寻求正确捕获,如果每一层都没有正确捕获,程序仍然会运行终止。
7.4.标准异常体系
C++给常见的异常类型进行了定义和分类,引入#include<stdexcept>头文件后即可使用。
但是这个体系还是太薄弱了。因此可以对其进行拓展。
自定义一个类型,继承自某个异常类型即可。
#include <iostream> #include <stdexcept> using namespace std; int main() { string s = "helloworld"; try // 尝试抛出一个异常 { cout << s.at(100) << endl; } catch(out_of_range &e) // 尝试捕获一个异常 { // 输出错误信息 cout << e.what() << endl; // 补救措施 cout << -1 << endl; } cout << "helloworld" << endl; return 0; }
一个抛出自定义异常的例子:
#include <iostream> #include <stdexcept> using namespace std; class MyException : public exception { public: // 覆盖what函数 // throw():异常规格说明,表示此函数不会出现任何异常的抛出 const char * what() const throw() { return "自定义异常类型"; } }; void show(string a,string b) { if(a == "#" || b == "#") { throw MyException(); } cout << a << b << endl; } int main() { cout << "请输入两个字符串" << endl; string a; string b; cin >> a >> b; try { show(a,b); } catch(MyException &e) { // 返回异常详细信息 cout << e.what() << endl; // 异常处理 } cout << "main执行结束" << endl; return 0; }
7.5.多重捕获
一个try块可以配合多个catch块同时匹配。
#include <iostream> #include <stdexcept> using namespace std; class MyException : public logic_error { public: MyException():logic_error("错了"){} // 覆盖what函数 // throw():异常规格说明,表示此函数不会出现任何异常的抛出 const char * what() const throw() { return "自定义异常类型"; } }; void show(string a,string b) { if(a == "#" || b == "#") { throw MyException(); } cout << a << b << endl; } int main() { cout << "请输入1或者2或者其他数字" << endl; int type; cin >> type; try { if(type == 1) { string s = "fafa"; cout << s.at(100) << endl; } else if(type == 2) { throw overflow_error("overflow_error异常"); } else { show("#","111"); } } catch(MyException &e) { // 返回异常详细信息 cout << e.what() << endl; // 异常处理 } catch(overflow_error &e) { cout << e.what() << "overflow_error异常捕获成功" << endl; } catch(out_of_range &e) { cout << e.what() << "out_of_range捕获成功" << endl; } cout << "main执行结束" << endl; return 0; }
7.6.粗略捕获
除了可以直接捕获标准类型外,也可以捕获异常的基类,甚至捕获所有类型。
#include <iostream> #include <stdexcept> using namespace std; int main() { string s = "fafa"; try { cout << s.at(100) << endl; } catch(exception &e) { cout << "成功" << endl; } return 0; }
也可以粗略捕获与多重捕获同时使用,此时要注意捕获的顺序为派生类异常优先。
#include <iostream> #include <stdexcept> using namespace std; class MyException : public logic_error { public: MyException():logic_error("错了"){} // 覆盖what函数 // throw():异常规格说明,表示此函数不会出现任何异常的抛出 const char * what() const throw() { return "自定义异常类型"; } }; void show(string a,string b) { if(a == "#" || b == "#") { throw MyException(); } cout << a << b << endl; } int main() { cout << "请输入1或者2或者其他数字" << endl; int type; cin >> type; try { if(type == 1) { string s = "fafa"; cout << s.at(100) << endl; } else if(type == 2) { throw overflow_error("overflow_error异常"); } else { show("#","111"); } } catch(MyException &e) { // 返回异常详细信息 cout << e.what() << endl; // 异常处理 } catch(overflow_error &e) { cout << e.what() << "overflow_error异常捕获成功" << endl; } catch(out_of_range &e) { cout << e.what() << "out_of_range捕获成功" << endl; } catch(exception &e) { cout << "成功" << endl; } cout << "main执行结束" << endl; return 0; }
使用...可以捕获所有类型,但是不推荐,更推荐使用或者建立标准异常体系。
#include <iostream> #include <stdexcept> using namespace std; class MyException : public logic_error { public: MyException():logic_error("错了"){} // 覆盖what函数 // throw():异常规格说明,表示此函数不会出现任何异常的抛出 const char * what() const throw() { return "自定义异常类型"; } }; void show(string a,string b) { if(a == "#" || b == "#") { throw MyException(); } cout << a << b << endl; } int main() { cout << "请输入1或者2或者其他数字" << endl; int type; cin >> type; try { if(type == 1) { string s = "fafa"; cout << s.at(100) << endl; } else if(type == 2) { throw overflow_error("overflow_error异常"); } else { show("#","111"); } } catch(MyException &e) { // 返回异常详细信息 cout << e.what() << endl; // 异常处理 } catch(overflow_error &e) { cout << e.what() << "overflow_error异常捕获成功" << endl; } catch(out_of_range &e) { cout << e.what() << "out_of_range捕获成功" << endl; } catch(...) // 可以捕获所有异常类型 { cout << "成功" << endl; } cout << "main执行结束" << endl; return 0; }
C++的异常处理机制不完善,是否使用取决于你的开发团队。
8.智能指针
8.1.概念
堆内存的对象需要手动delete销毁,如果忘记使用delete销毁就会造成内存泄漏的问题。
所以在C++ 98 标准中引入了智能指针的概念,并在C++11 标准中趋于完善。
使用智能指针可以让堆内存具有栈内存对象的特点,原理是给需要手动回收的堆内存对象套上一个栈内存的模板类对象即可。
C++有4种智能指针:
- auto_ptr(自动指针)(已废弃 C++ ISO 98)
- unique_ptr(唯一指针)(C++ 11)
- shared_ptr (共享指针)(C++ 11)
- weak_ptr (虚指针) (C++ 11)
使用智能指针都需要引入头文件#include <memory>
8.2.auto_ptr
#include <iostream> #include <memory> using namespace std; class Test { private: string s; public: Test(string s):s(s) { cout << s << "构造函数" << endl; } ~Test() { cout << s << "析构函数" << endl; } void show() { cout << s << "程序执行" << endl; } }; int main() { // 局部代码块 { Test *t1 = new Test("A"); // 创建一个智能指针栈内存对象 auto_ptr<Test> ap1(t1); // ap1 管理 t1 // 取出被管理的堆内存对象,并调用show成员函数 ap1.get()->show(); // 释放ap1智能指针对t1对象的控制权 // ap1.release(); // 释放控制权并销毁资源对象 // ap1.reset(); // 创建B堆区对象,B把A顶掉,A对象销毁。 ap1.reset(new Test("B")); cout << "局部代码块执行结束" << endl; } cout << "程序运行结束" << endl; return 0; }
由于成员变量存在指针类型,因此拷贝构造函数与赋值运算符的使用会出现问题,与浅拷贝不同的是,auto_ptr的复制语义会引起资源对象控制权转移的问题。
#include <iostream> #include <memory> using namespace std; class Test { private: string s; public: Test(string s):s(s) { cout << s << "构造函数" << endl; } ~Test() { cout << s << "析构函数" << endl; } void show() { cout << s << "程序执行" << endl; } }; int main() { // 局部代码块 { auto_ptr<Test> ap1(new Test("A")); auto_ptr<Test> ap2(ap1); // 拷贝构造函数 cout << ap1.get() << " " << ap2.get() << endl; // 0 0xff1110 auto_ptr<Test> ap3 = ap2; // 拷贝构造函数 // 0 0 0xf71110 cout << ap1.get() << " " << ap2.get() << " " << ap3.get() << endl; auto_ptr<Test> ap4; ap4 = ap3; // 赋值运算符重载 // 0 0 0 0x701110 cout << ap1.get() << " " << ap2.get() << " " << ap3.get() << " " << ap4.get() << endl; cout << "局部代码块执行结束" << endl; } cout << "程序运行结束" << endl; return 0; }
8.3.unique_ptr
格式:unique_ptr<类名>智能指针名(new 类名(根据需要以及构造函数进行赋值))
作为对auto_ptr的改进,unique_ptr对其他持有的资源对象具有唯一控制权,即不能通过常规的复制语法转移或者拷贝资源对象的控制权。(会报错)
但是unique_ptr可以通过特殊的语法来先实现控制权转移效果。
#include <iostream> #include <memory> using namespace std; class Test { private: string s; public: Test(string s):s(s) { cout << s << "构造函数" << endl; } ~Test() { cout << s << "析构函数" << endl; } void show() { cout << s << "程序执行" << endl; } }; int main() { // 局部代码块 { unique_ptr<Test> ap1(new Test("A")); unique_ptr<Test> ap2(move(ap1)); // 拷贝构造函数 cout << ap1.get() << " " << ap2.get() << endl; // 0 0xff1110 unique_ptr<Test> ap3 = move(ap2); // 拷贝构造函数 // 0 0 0xf71110 cout << ap1.get() << " " << ap2.get() << " " << ap3.get() << endl; unique_ptr<Test> ap4; ap4 = move(ap3); // 赋值运算符重载 // 0 0 0 0x701110 cout << ap1.get() << " " << ap2.get() << " " << ap3.get() << " " << ap4.get() << endl; cout << "局部代码块执行结束" << endl; } cout << "程序运行结束" << endl; return 0; }
8.4.shared_ptr
unique_ptr对资源具有独占性,多个shared_ptr 对象可以共享资源。
shared_ptr有两种创建方式:
格式1:shared_ptr<类名>智能指针名=(new 类名(根据需要以及构造函数进行赋值))
新方式2:shared_ptr<类名>智能指针名= make_shared<类名>(根据需要以及构造函数进行赋值)
两种创建方式的区别在于后者是一步实现(创建资源对象+关系绑定),前者分为两步完成(先创建资源对象再关系绑定)。
后者优点:
- 安全性更好
- 性能更高
后者缺点:
- 资源释放效率低
每次多一个shared_ptr对资源进行管理,引用计数将+1,每个指向该对象的shared_ptr对象销毁时,引用计数将-1,最后一个shared_ptr对象销毁时,计数清零,资源对象销毁。
#include <iostream> #include <memory> using namespace std; class Test { private: string s; public: Test(string s):s(s) { cout << s << "构造函数" << endl; } ~Test() { cout << s << "析构函数" << endl; } void show() { cout << s << "程序执行" << endl; } }; int main() { shared_ptr<Test> sp3; // 局部代码块 { shared_ptr<Test> sp1 = make_shared<Test>("A"); cout << sp1.use_count() << endl; // 1 shared_ptr<Test> sp2(sp1); // 拷贝构造函数 cout << sp2.use_count() << endl; // 2 cout << sp1.use_count() << endl; // 2 sp3 = sp2; cout << sp3.use_count() << endl; // 3 shared_ptr<Test> sp4 = make_shared<Test>("B"); cout << sp4.use_count() << endl; // 1 } cout << sp3.use_count() << endl; // 1 cout << "程序运行结束" << endl; return 0; }
8.5.weak_ptr(虚指针)
格式:weak_ptr<类名>智能指针名=(new 类名(根据需要以及构造函数进行赋值))
weak_ptr是一个不控制资源对象的智能指针,也不会影响资源的引用计数,其主要的目的是协助shared_ptr工作。
通过weak_ptr的构造函数,参数传入一个持有资源对象的shared_ptr对象或者weak_ptr对象即可创建。
weak_ptr与资源对象呈现弱相关性,不支持get等函数直接操作资源对象。
建议weak_ptr调用lock函数之前,先检测引用计数是否大于0,或者使用expirend()检测是否可以转换为shared_ptr。
lock()
函数用于获取一个指向weak_ptr
所管理对象的shared_ptr
。如果weak_ptr
所指向的对象已经被销毁,lock()
会返回一个空的shared_ptr
。expired()
函数用于检查weak_ptr
所指向的对象是否已经被销毁。如果对象已被销毁,返回true
;否则返回false
。#include <iostream> #include <memory> using namespace std; class Test { private: string s; public: Test(string s):s(s) { cout << s << "构造函数" << endl; } ~Test() { cout << s << "析构函数" << endl; } void show() { cout << s << "程序执行" << endl; } }; int main() { weak_ptr<Test> wp3; // 局部代码块 { shared_ptr<Test> sp1 = make_shared<Test>("A"); weak_ptr<Test> wp1 = sp1; cout << sp1.use_count() << endl; // 1 cout << wp1.use_count() << endl; // 1 weak_ptr<Test> wp2(wp1); // 拷贝构造 cout << wp2.use_count() << endl; // 1 // 从weak_ptr中得到一个持有资源对象的shared_ptr对象 shared_ptr<Test> sp2 = wp1.lock(); sp2.get()->show(); cout << sp2.use_count() << endl; // 2 wp3 = wp2; } if(wp3.expired()) { cout << "无法使用lock函数" << endl; } else { cout << "可以使用" << endl; } cout << wp3.use_count() << endl; // 0 cout << "程序运行结束" << endl; return 0; }
九、其他
- nullptr(掌握)
NULL在源码中就是个0,因此可能会存在一些二义性得问题。
#include <iostream> using namespace std; void func(int a) { cout << "a = " << a << endl; } void func(int *b) { cout << "b = " << b << endl; } int main() { func(NULL); // a = 0 return 0; }
在c++11中使用nullptr代替NULL,作为空指针得表示方式,在C++中,可以用作空指针常量,表示指针不指向任何有效的空间地址,
#include <iostream> using namespace std; void func(int a) { cout << "a = " << a << endl; } void func(int *b) { cout << "b = " << b << endl; } int main() { func(nullptr); return 0; }
- 类型推导(掌握)
使用auto关键字可以推导类型,C++11引入的。
#include <iostream> using namespace std; int main() { auto i = 10; // i 的类型被推导为整形(int) cout << i << endl; auto i2 = 19.2; // i2 的类型被推导为浮点型(double) cout << i2 << endl; auto i3 = new auto(10); // i3 的类型被推导为int * cout << *i3 << endl; auto i4 = 'a'; cout << i4 << endl; delete i3; return 0; }
decltype可以推导表达式的类型,但是需要注意的是,decltype只会分析表达式的类型,不会具体计算表达式的值。
#include <iostream> using namespace std; int main() { auto x = 1; auto y = 2; decltype(x * y + 123) z = 8888.24; // int * int + int = int cout << z << endl; // 8888 return 0; }
- 初始化列表(熟悉)
C++11中引入列表初始化(初始化列表、通用统一初始化、一致性初始化)语法,可以使用{}对对象进行初始化。
#include <iostream> #include <array> #include <vector> using namespace std; class Student { private: string name; int age; public: Student(string name,int age):name(name),age(age){} void show() { cout << name << " " << age << endl; } }; int main() { array<int,5> arr1 = {1,2,3,4,5}; for(auto i :arr1) { cout << i << endl; } cout << "-=-=-----------------" << endl; array<int,5> arr2 = {1,2,3}; for(auto i :arr2) { cout << i << endl; } cout << "-=-=-----------------" << endl; vector<int> vec1 = {1,1,1}; for(auto i :vec1) { cout << i << endl; } Student s1("张三",12); s1.show(); Student s2 = {"李四",24}; s2.show(); return 0; }
4、面试题(重点)
【面试题】C++11学过的特性有那些?
- 类型推导
- 智能指针(后三个)
- 初始化列表
- for each
- array
- 类型转换(四种cast函数)
- 继承构造
- nullptr
- override
5、进制输出(了解)
C++11 可以对整数进行不同进制的输出。
#include <iostream> using namespace std; int main() { // 为了区分不同进制,可以增加进制显示功能,此功能设定也是持久的 cout << showbase; cout << dec << 1234 << endl; // 1234 cout << oct << 1234 << endl; // 2322 // 输出进制的设定是持久的 cout << 9 << endl; // 11 cout << hex << 256 << endl; // 100 // 取消进制显示功能 cout << noshowbase; cout << 16 << endl; // 10 return 0; }
6、设定输出域宽度(了解)
当使用setw()来指定一个整数或者一个字符串输出占用的域宽度
- 当设定域宽度小于数据本身时,仍然会显示为数据本身的宽度。
- 当设定域宽度大于数据本身时,会显示为设定的宽度。
#include <iostream> #include <iomanip> using namespace std; int main() { // 仍然按照实际的宽度输出 cout << setw(5) << 123456 << setw(5) << 123456 << endl; // 使用输出域宽度 cout << setw(10) << 123456 << setw(10) << 123456 << endl; cout << setw(10); cout << 123456 << endl; cout << 123456 << endl; // 输出域无效,因为输出域只能作用于下一行 return 0; }
7、文件IO(了解)
#include <iostream> #include <fstream> using namespace std; int main() { // 文件流对象,用于读取数据 // 参数1:读取的路径 // 参数2:读取的方式为二进制 ifstream ifs("D:\\meeting_01.mp4",ios_base::binary); if(!ifs) { cout << "文件或路径不存在" << endl; return -1; } // 文件输出流对象,用于输出数据 // 参数1:写出的路径 // 参数2:写出方式为二进制 ofstream ofs("C:\\Users\\Administrator\\Desktop\\meeting_01.mp4",ios_base::binary); if(!ofs) { cout << "文件或路径不存在" << endl; return -1; } // 把文件指针移动输入流尾部 ifs.seekg(0,ios::end); cout << "文件总大小:" << ifs.tellg() << "bytes" << endl; // 把文件指针移动回头部 ifs.seekg(0,ios::beg); // 准备一个buffer char buf[2048]; long long hasRead = 0; // 已经读取的文件大小 while(ifs) { // 读固定的长度到buf中 ifs.read(buf,2048); // 输出设定宽度的数据到目标位置 // 参数1:数据来源 // 参数2:输出的数据量ifs.gcount()为上一次文件指针的偏移量 ofs.write(buf,ifs.gcount()); hasRead += ifs.gcount(); cout << "已经拷贝字节数:" << hasRead << endl; } // 收尾 ifs.close(); ofs.flush(); // 清空缓存区 ofs.close(); return 0; }