C++基础

1 指针

1.1空指针和野指针

空指针:指针变量指向内存中编号为0的空间的

用途:初始化指针变量

注意:空指针指向的内存是不可以访问的

1
2
3
4
5
int main(){
int *p = null;//空指针
*p = 100;
//会引发异常,0~255之间内存空间为系统内存占用,不允许访问
}

野指针:指针指向非法的内存空间

1
2
3
4
5
int main(){
int *p = (int *)0x1100;//野指针
std::cout << *p << std::endl;
//访问野指针报错
}

总结:空指针和野指针都不是我们申请的空间,因此不要访问。

1.2 const修饰指针

const修饰指针有三种情况:

  1. const修饰指针-常量指针
  2. const修饰常量-指针常量
  3. const既修饰指针,又修饰常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(){
int a = 10;
int b = 10;

//const修饰的是指针,指针的指向可以改,指针指向的值不可以改
const int *p1 = &a;
p1 = &b;//正确
//*p1 = 100; 报错

//const修饰的是常量,指针不可以改指针指向的值可以改
int * const p2 = &a;
//p2 = &b; 报错
*p2 = b;//正确

//const既修饰指针又修饰常量,值都不可以改
const int * const p3 = &a;
//p3 = &b; 错误
//*p3 = b; 错误
}

1.3指针和数组

1
2
3
4
5
6
7
8
9
10
int main(){
using namespace std;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int * p = arr;
cout << *p << endl;
for(int i = 0;i < 10;i++){
//p是一个整形的指针,加一后地址向后移动4字节
cout<< *p++ << endl;
}
}

1.4指针和函数

作用:利用指针作函数的参数,可以修改实参的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using namespace std;

//地址传递,可以修改实参的值
void swap(int *p1,int *p2){
int tmep = *p1;
*p1 = *p2;
*p2 = temp;
}

int main(){
int a = 10;
int b = 5;
swap(10,5);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}

1.5冒泡排序

功能描述:封装一个函数,利用冒泡排序,实现对整型数组的升序排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

void bubbleSort(int *arr,int length){
for(int i = 0;i < length - 1;i++){
for(int j = 0;j < length - 1 -i;j++){
if(*arr > *arr+i){
int tmp = *arr+i;
*arr+i = *arr;
*arr = tmp;
}
}
}
}
int main(){
using namespace std;
int arr[5] = {1,2,3,4,5};
bubbleSort(arr,5);
for(int i = 0;i < 5;i++){
cout << arr[i] << endl;
}
}

2 结构体

2.1结构体的基本概念

结构体属于用户自定义的数据内型,允许用户储存不同的数据类型。

2.2结构体定义和使用

语法:struct 结构体名 {结构体成员列表};

通过结构体创建变量名的方式有三种:

  • struct 结构体名 变量名
  • struct 结构体名 变量名 = {成员值1,成员值2...}
  • 定义结构体时顺便创建变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Student
{
//成员列表
string name;
int age;
int score;
};

int main(){
//方式一 创建结构体变量
struct Student s1;//struct关键字可以省略
s1.name = "xxx";
s1.age = 18;
s1.score = 100;

//方式二
struct Student s2 = {"Qingren",20,100};
}
1
2
3
4
5
6
//方式三
struct Student{
string name;
int age;
int score;
}s3;

2.3结构体数组

作用:将自定义的结构体放入到数组中方便维护

语法struct 结构体名 数组名[元素个数] = { {},{},{}...}

2.4结构体指针

作用:通过指针访问结构体中的成员

  • 利用操作符->可以通过结构体指针访问结构体属性
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Student{
string name;
int age;
int score;
};

int main(){
Student s1 = {"Qingren",20,100};
Student *p = &s1;
cout << "name:" << p->name << endl;
cout << "age:" << p->age << endl;
cout << "score:" << p->score << endl;
}

2.5结构体嵌套结构体

作用:结构体中的成员可以是另一个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义学生结构体
struct Student{
string name;
int age;
int score;
};

//定义教师结构体
struct Teacher{
int id;
string name;
int age;
struct Student stu;
};

2.6结构体作为函数参数

作用:将结构体作为参数向函数中传递

传递方式有两种:

  • 值传递
  • 引用传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//学生结构体定义
struct student
{
string name;
int age;
int score;
};

//值传递
void printStudent(Student stu){
stu.age = 20;
cout << stu.name <<endl;
}

//引用传递
void printStudent_(Student *stu){
cout << stu->name << endl;
}

2.7结构体中const的使用场景

作用:用const防止误操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//学生结构体定义
struct student
{
string name;
int age;
int score;
};

//值传递
void printStudent(Student stu){
stu.age = 20;
cout << stu.name <<endl;
}

//引用传递,如果结构体的成员变量太多,通过值传递的形式,需要复制出一个副本,消耗大量内存,通过const修饰结构体指针,禁止修改指针所指内容
void printStudent_(const Student *stu){
//stu->age = 15; 错误
cout << stu->name << endl;
}

C++核心

1 内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数的二进制代码,由操作系统进行管理
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放内存,存放函数的参数值、局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

不同区域存放的数据,赋予不同的生命周期,使编程更灵活

栈区数据注意事项

不要返回局部变量地址,局部变量存放在栈区,栈区的数据在函数执行完成后会自动释放

堆区

  • 在堆区开辟空间:

    1
    2
    3
    4
    5
    int * func (){
    //利用new关键字可以将数据开辟到堆区
    int *p = new int(10);
    return p;
    }

1.1 new操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

int * fuc(){
int * p = new int(10);
return p;
}

int *fuc2(){
//创建10整型的数组在堆区
int * p = new int[10];
for(int i = 0;i < 10;i++){
*(p+i) = 100 + i;
}
return p;
}

int main()
{
int *p = fuc();
cout << *p << endl;
int *p2 = fuc2();
for(int i = 0;i < 10;i++){
cout << *(p2 + i) << endl;
}
}

2 引用

2.1引用的基本使用

作用:给变量取别名

语法数据类型 &别名 = 原名

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
using namespace std;

int a = 10;
int &b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;

b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;

}

2.2引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变
1
2
3
4
5
6
int main(){
int a = 10;
int &b;//错误 引用必须初始化 int &b = a;
}


2.3引用作函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点: 可以简化指针修改实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//值传递
void swap_1(int a,int b){
int tmp = a;
a = b;
b = tmp;
}

//地址传递
void swap_2(int * a ,int * b){
int tmp = *a;
*a = *b;
*b = tmp;
}

//引用传递
void swap_3(int &a,int &b){
int tmp = a;
int a = b;
b = tmp;
}

2.4引用作函数的返回值

作用:引用是可以作为函数的返回值存在的

注意:不要返回局部变量引用

引用:函数调用作为左值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//不要返回局部变量引用
int& test01(){
int a = 10;//局部变量放在栈区
return a;
}

//返回静态变量引用
int& test02(){
static int a = 10;//静态变量放在全局区
return a;
}

int main(){
using namespace std;
int &ref2 = test02();//ref2也为a的引用
cout << "ref2 = " << ref2 << endl;

test02() = 1000;//返回的是静态变量a的引用

cout << "ref2 = " << ref2 << endl;
}

2.5引用的本质

本质:引用的本质在c++内部实现是一个指针常量

引用一旦初始化后,就不可发生改变是因为在指针常量中有const修饰,不可以再指向别的空间

1
2
3
4
5
6
7
8
9
10
int main(){
int a = 10;

//自动转换为 int * const ref = &a;指针常量是指针指向不可改,这也说明为什么引用不可改
int& ref = a;
ref = 20;//内部发现ref是引用,自动帮我们转化为:*ref = 20;

cout << "a = " << a << endl;
cout << "ref = " << ref << endl;
}

2.6常量引用

作用:常量引用主要是用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
//int & ref = 10; 错误 不能够引用常量
//加上const之后编译器将代码修改 int temp = 10;const int & ref = temp;
const int & ref = 10;//引用必须引一块合法的内存空间
// ref = 20; ref 错误 加入const之后,变为只读,不可修改
}

void showValue(const int & a){
//val = 100; 错误 使用const 修饰为引用常量,不能修改
//这样用来修饰形参,防止误操作

cout << "val = " << val << endl;
}

3 函数的提高

3.1形参的默认值

在C++中,函数的形参列表中的形参是可以有默认值的;

语法:返回值类型 函数名 (参数 = 默认值){}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//如果我们自己传入数据,就用自己的数据,如果没有,就用默认值
int fun(int a, int b = 20, int c = 30){
return a+b+c;
}

//如果某个位置已经有默认参数,那么从这个位置往后,从左到右都必须有默认值
//int fun2(int a,int b = 10,int c,int d){} 错误

//如果函数的声明有默认参数,那么函数的实现就不能有默认参数了
//声明和实现只能一个有参数
int fun3(int a = 10,int b = 20);
//int fun3(int a = 10,int b = 20){} 错误
int fun3(int a,int b){
return a+b;
}

3.2函数占位参数

C++函数的形参列表中可以有占位参数,用来做占位,调用函数时必须填补该位置

语法:返回值类型 函数名 (数据类型){}

1
2
3
4
5
6
7
8
9
10
//函数占位参数,占位参数也可以有默认参数
//void func(int a,int = 10)
void func(int a,int){
cout << "this i func" << endl;
}

int main(){
func(10,10);//占位符必须填补

}

3.3函数的重载

3.3.1函数重载概述

作用:函数名可以相同,提高复用性

函数重载满足的条件

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同或则个数不同或者顺序不同

注意:函数的返回值不可以作为函数重载的条件

3.3.2函数重载的注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void fun(int &a){
cout << "fun(int &a)" << endl;
}

void fun(const int &a){
cout << "fun(const int &a)" << endl;
}

int main(){
int a = 10;
func(a);//调用的时fun(int &a),因为a相当于是一个可读可写的变量
func(10);//调用fun(const int &a)
//调用fun(int &a)-- int &a = 10不合法,因为10是一个常量
}
1
2
3
4
5
6
7
8
9
10
11
void fun2(int a,int b = 10){
cout << "fun2(int a,int b)" << endl;
}

void fun2(int a){
cout << "fun2(int a)" << endl;
}

int main(){
//fun2(5); 错误,出现了二义性,报错,要尽量避免
}

4 类和对象

C++面向对象的三大特性:封装、继承、多态

C++认为万事万物皆为对象,对象上有其属性和行为

4.1封装

4.1.1封装的意义

封装是C++面向对象的三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事务
  • 将属性和行为加以权限控制

封装意义一

在设计类的时候,属性和行为写在一起,表现事务

语法:class 类名{访问权限:属性/行为};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Circle{
//访问权限
public
//属性
int m_r;
//行为
public
double calculateZC(){
return 2*PI*m_r;
}
};

int main(){
Circle c1;//实例化
cl.mr = 10;

cout << "圆的周长 = " << c1.calculateZC() << endl;
}

封装意义二

类在设计的时候,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

1.public 公共权限 类内可以访问,类外可以访问

2.protected 保护权限 类内可以访问,类外不可以访问,子类能够访问

3.private 私有权限 类内可以访问,类外不可以访问,子类无法访问

4.1.2struct和class的区别

在C++中struct和class唯一的区别就在于 默认的访问权限不同

区别:

  • struct默认权限为公共
  • class默认权限为私有

4.1.3成员属性设置为私有

优点1:将成员属性设置为私有,可以自己控制读写权限

优点2:对于写权限,我们可以检测数据的有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person{
private:
int age;
string name;
public:
void setAge(int age_){
if(age_<0){
return;
}
age = age_;
}
void setName(int name_){
if(name_ < 0){
return;
}
name = name_;
}

int getAge(){
return age;
}
string getName(){
return name;
}
};

4.2对象的初始化和清理

4.2.1构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

一个对象或者变量没有初试状态,对其使用的后果是未知的

同样使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要求我们做的事情,因此如果我们不提供构造和折构,编译器会提供编译器提供的构造函数和折构函数是空实现的

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法:~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构函数,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person{
public:
//构造函数
Person(){
cout << "Person 构造函数调用" << endl;
}
//析构函数
~Person(){
cout << "Perosn 析构函数调用" << endl;
}
};

int main(){

}

4.2.2构造函数的分类及调用

两种分类方式:

  • 按参数分类:有参构造和无参构造
  • 按类型分类:普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显式法
  • 隐式转换法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Person{
public:
//无参构造
Person(){

}
//有参构造
Person(int a){

}
//拷贝构造
Person(const Person &p){
age = p.age;
}
private:
int age;
};

//调用方法
void test01(){
//1、括号法
Person p1;//默认构造函数调用,注意不要加()
Person p2(10);//有参构造函数
Person p3(p2);//拷贝构造函数
//2、显式法
Person p1;
Person p2 = Person(10);//有参构造
Person p3 = Person(p2);//拷贝构造

Person(10);//匿名对象 特点:当前行执行完后,系统会立即回收掉匿名对象
//Person(p3); 错误,注意不要使用拷贝构造函数初始化匿名对象,编译器会认为Person(p3)->Person p3;

//3、隐式转换法
Person p4 = 10;//有参构造->Person p4 = Person(10);
Perosn p4 = p5;//拷贝构造
}

4.2.3拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//情况一
int main(){
Person p1;
Person p2 = Person(p1);
}
//情况二
void doWork(Person p){

}
int main(){
Person p1;
doWork(p1);
}
//情况三
Person doWork(){
Perosn p1;
return p1;//返回的是p1对象的拷贝
}
int main(){
doWork();
}

4.2.4构造函数调用规则

默认情况下,c++编译器至少给一个类添加三个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝函数,对属性进行拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝函数
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数

4.2.5深拷贝与浅拷贝

深拷贝是经典面试题,也是常见的坑

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题就是堆区内存的重复释放

image-20200912090643041

深拷贝通过重新创建一个堆区内存,解决堆区内存重复释放的问题:

image-20200912090759537

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;

class Person{
public:
Person(){
cout << "无参构造函数" << endl;
}
Person(int age,int height){
cout << "有参构造函数" << endl;
m_age = age;
m_height = new int(height);//开辟堆空间
}
//深拷贝
Person(const Person &p){
cout << "拷贝构造函数" << endl;
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//在析构函数中释放堆空间
~Person(){
cout << "析构函数" << endl;
if(m_height != NULL){
delete m_height;
m_height = NULL;
}
}
int getAge(){
return m_age;
}
int getHeight(){
return *m_height;
}
private:
int m_age;
int * m_height;
};

void test01(){

}

int main(){
Person p1 = Person(10,180);
Person p2 = Person(p1);
cout << p2.getAge() << "/"<<p2.getHeight() << endl;
}

4.2.6初始化列表

作用:C++提供了初始化列表的语法,用来初始化属性

语法:构造函数():属性1(值1),属性2(值2)...()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
public:
//传统初始化
Person(int a,int b,int c){
m_a = a;
m_b = b;
m_c = c;
}
//初始化列表的方式
Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){}
//Person():m_a(10),m_b(20),m_c(30){}
private:
int m_a;
int m_b;
int m_c;
}
int main(){
Person p1(30,20,10);
}

4.2.7类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

1
2
3
4
class A{}
class B{
A a;
}

当其它类对象作为本类成员,构造时先构造类对象,再构造自身

析构时,先析构自身,再析构类对象

4.2.8静态成员

静态成员就是在成员变量和成员函数之前加上关键字static,称为静态成员

静态成员分为:

  • 静态成员变量
    1. 所有对象共享同一份数据
    2. 在编译阶段分配内存
    3. 类内声明,类外初始化
  • 静态成员函数
    1. 所有对象共享同一个函数
    2. 静态成员函数只能访问静态成员变量
    3. 静态成员函数也是有访问权限的,private 修饰的话,无法类外访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
static void func(){
cout << "func函数调用" << endl;
m_A = 10;
//m_B = 20; 错误 静态函数无法调用非静态成员变量
}

static int m_A;
int m_B = 200;
}
//类内声明,必须在类外初始化
int Person::m_A = 10;

int main(){
//直接通过类调用
Person::func();
//通过对象调用
Person p1;
p1.func();
}

4.3 C++对象模型和this指针

4.3.1成员变量和成员函数分开储存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class People{
int m_A;//非静态变量 属于类的对象上
satic int m_B;//静态成员变量 不属于类对象上
void func(){}//非静态成员函数 不属于类对象
static void func2(){}//静态成员函数 不属于类的对象上
};
int Person::m_B = 0;

void test01(){
Person p;
//空对象占用内存空间为1
//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占用内存的位置,而且每个空对象也应该有一个独一无二的内存地址
cout << "size of p = " << sizeof(p) << endl;
}
int main(){

}

4.3.2this指针的概念

C++通过提供特殊的对象指针,this指针,this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途

  • 当形参和成员变量同名时,可以用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return * this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person{
Person(int age){
this->age = age;
}
Perosn& addAge(Person &p){
this->age += p.age;
return *this;
}
private:
int age;
}

int main(){
Person p1 = Person(10);
Person p2 = Person(10);

p2.addAge(p1).addAge(p1).addAge(p1);
cout << p2.age << endl;//输出40
}
//如果addAge函数返回对象而不是对象的引用,则输出20
//因为每次调用,返回的都是对象的拷贝,而不是对象本身
//Perosn addAge(Person &p){
// this->age += p.age;
// return *this;
// }

4.3.3空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码健壮性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
public:
void showClassName(){
cout << "我是Person类" << endl;
}
void showPerson(){
if(this == NULL){
return;
}
cout << "name = " << name << endl;
}
private:
string name;
}

int main(){
Person *p = NULL;
p->showClassName();
//p->showPerson(); 错误
}

4.3.4const修饰成员函数

常函数

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person{
//this指针的本质是指针常量,指针的指向是不可以修改的
//在成员函数后面添加const,修饰的是this指向,让指针指向的值也不能修改
void showPerson() const{
//this->m_A = 100; 错误
//this = NULL; 错误
this->m_B = 200;
}

int m_A;
mutable int m_B;//特殊变量,即使在常函数中,也可以修改这个值,加关键字mutabel
}

void test02()
{
const Person p;//在对象前加const 变为常对象
//p.m_A = 100; 错误
p.m_B = 100;//m_B是特殊值,在常对象下也可以修改

//常对象只能调用常函数
p.showPerson();
}

4.4友元

4.4.1全局函数作友元

友元的目的就是让一个函数或者类访问另一个类中的私有成员

友元的关键字为friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
1
2
3
4
5
6
7
8
9
10
11
12
class Building{
//goodFriend可以访问Building中的私有成员
friend void goodFriend(Buiding *bulding);
public:
string m_SittingRoom;//客厅
private
string m_BedRoom;
}

void goodFriend(Buiding *bulding){
cout << "好朋友全局函数" << building ->m_BedRoom << endl;
}

4.4.2类做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Building{
//类做友元
friend class GoodFriend;
public:
Building();//在类内声明,在类外实现
public:
string m_SittingRoom;
private:
string m_BedRoom;
}

class GoodFriend{
public:
GoodFriend();
void visit();
Building *building;
}

Building::Building(){
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}

GoodFriend::GoodFriend(){
this->building = new Building;
}

void GoodFriend::visit(){
cout << "好友正在访问" << building->m_SittingRoom << endl;
//GoodFriend类中的方法可以访问Building类中的私有成员变量
cout << "好友正在访问" << building->m_BedRoom << endl;
}

4.4.3成员函数做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Building{
//类做友元
friend void GoodFriend::visit2();
public:
Building();//在类内声明,在类外实现
public:
string m_SittingRoom;
private:
string m_BedRoom;
}

class GoodFriend{
public:
GoodFriend();
void visit1();
void visit2();
Building *building;
}

Building::Building(){
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}

GoodFriend::GoodFriend(){
this->building = new Building;
}

void GoodFriend::visit1(){
cout << "好友正在访问" << building->m_SittingRoom << endl;
//该函数无法访问Building类的私有变量
}

void GoodFriend::visit2(){
cout << "好友正在访问" << building->m_SittingRoom << endl;
//该方法作为Buidling类的友元,可以访问Building类中的私有成员变量
cout << "好友正在访问" << building->m_BedRoom << endl;
}

4.5运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其一种功能,以适应不同的数据类型

4.5.1加号运算符重载

作用:实现两个自定义数据类型相加的运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
using namespace std;

class Person{
public:
Person(){}

Person(int a,int b){
this->a = a;
this->b = b;
}

//成员函数重载加号
Person operator+(const Person &p2){
Person tmp = Person(*this);
tmp.a += p2.a;
tmp.b += p2.b;
return tmp;
}

public:
int a;
int b;
};

//全局函数重载加号
// Person operator+(Person &p1,Person &p2){
// Person tmp;
// tmp.a = p1.a + p2.a;
// tmp.b = p1.b + p2.b;
// return tmp;
// }

//全局函数重载加号
Person operator+(Person &p1,int x){
Person p = Person(p1.a + x, p1.b + x);
return p;
}

int main(){
Person p1 = Person(10,20);
Person p2 = Person(100,200);
Person p3 = p1 + p2;
Person p4 = p1 + 20;
cout << "a = " << p3.a << endl;
cout << "b = " << p3.b << endl;

cout << "a = " << p4.a << endl;
cout << "b = " << p4.b << endl;
return 0;
}

4.5.2左移运算符重载

作用:可以输出自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//左移运算符重载
#include <iostream>

using namespace std;

class Person{
public:
Person(){}

Person(int a,int b){
this->a = a;
this->b = b;
}

int getA(){
return this->a;
}

int getB(){
return this->b;
}

private:
int a;
int b;
};

//只能通过全局函数重载左移运算符
ostream & operator<<(ostream &cout,Person &p){
cout << "a = " << p.getA() << " b = " << p.getB() << endl;
return cout;
}

int main(){
Person p1 = Person(10,20);
cout << p1 <<endl;
}

4.5.3递增运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

using namespace std;

class MyInteger{
friend ostream & operator<<(ostream & cout,MyInteger &myint);
public:
MyInteger(){}

MyInteger(int x){
this->x = x;
}

//重载左++
MyInteger & operator++(){
++(this->x);
return *this;
}
//重载右++
MyInteger operator++(int){
MyInteger tmp = *this;
(this->x)++;
return tmp;
}

private:
int x;
};

ostream & operator<<(ostream & cout,MyInteger &myint){
cout << "x = " << myint.x;
return cout;
}


int main(){
MyInteger m = MyInteger(1);
cout << m++ << endl;
cout << m << endl;
}

4.5.4赋值运算符重载

C++编译器至少给一个类添加4个函数

  1. 默认构造函数
  2. 默认析构函数
  3. 默认拷贝函数
  4. 赋值运算符operator=,对属性进行拷贝

如果类中右属性指向堆区,做赋值操作时也会出现深拷贝和浅拷贝的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <string>
using namespace std;

class Person{
public:
Person(){}

Person(int age){
//this->age = age;
this->age = new int(age);
}

~Person(){
if(age != NULL){
delete age;
age = NULL;
}
cout << "调用析构函数" << endl;
}

//重载赋值运算符
Person &operator=(Person &p1){

cout << "赋值运算符重载" << endl;
this->age = new int(*p1.age);
return *this;
}

int getAge(){
return *this->age;
}

private:
int *age;
};

int main(){
Person p1 = Person(18);
Person p2 = p1;

cout << "p1.Age = " << p1.getAge() << endl;
cout << "p2.Age = " << p2.getAge() << endl;
}

4.5.5关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <string>

using namespace std;

class Person{
public:
Person(){}
Person(string name,int age){
this->name = name;
this->age = age;
}

string getName(){
return this->name;
}

int getAge(){
return this->age;
}
//重载==运算符
bool operator==(Person &p1){
if(this->getName() == p1.getName() && this->getAge() == p1.getAge()){
return true;
}else{
return false;
}
}
//重载!=运算符
bool operator!=(Person &p1){
if(*this == p1) return false;
else return true;
}


private:
string name;
int age;
};

int main(){
Person p1 = Person("Tom",18);
Person p2 = Person("Jerry",18);

// if(p1 == p2)
// cout << "p1 = p2" << endl;
// else
// cout << "p1 != p2" << endl;

if(p1 != p2)
cout << "p1 != p2" << endl;
else
cout << "p1 = p2" << endl;

}

4.5.6函数调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此被称为仿函数
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>

using namespace std;

class MyPrint{
public:
void operator()(string text){
cout << text << endl;
}
};

//仿函数非常的灵活,没有固定的写法
class MyAdd{
public:
int operator()(int x,int y){
return x + y;
}
};

int main(){
MyPrint mp;
mp("hello world!");//使用起来非常类似于函数的调用,因此被称为仿函数
MyAdd ma;
cout << ma(1,2) << endl;

//使用匿名函数对象
cout << MyAdd()(100,200) << endl;
}

4.6继承

继承是面向对象的三大特性之一

4.6.1继承的基本语法

class A:public B

A类称为子类或派生类

B类称为父类或基类

继承的好处:减少重复代码

4.6.2继承方式

继承的语法:class 子类 :继承方式 方式

继承方式一共有三种

  • 公共继承
  • 保护继承
  • 私有继承

image-20200915162841352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Father{
public:
int a;
protected:
int b;
private:
int c;
}

class Son :public Father{
public:
void func(){
a = 10;//父类中的公共权限成员->子类中依然是公共权限
b = 10;//父类中的保护权限成员->子类中依然是保护权限
//c = 10; 父类中的私有权限成员,子类访问不到
}
};

class Son2 :protected Father{
public:
void func(){
a = 10;//父类中的公共权限成员->子类中是保护权限
b = 10;//父类中的保护权限成员->子类中依然是保护权限
//c = 10; 父类中的私有权限成员,子类访问不到
}
};

class Son2 :protected Father{
public:
void func(){
a = 10;//父类中的公共权限成员->子类中是保护权限
b = 10;//父类中的保护权限成员->子类中依然是保护权限
//c = 10; 父类中的私有权限成员,子类访问不到
}
};

class Son3 :private Father{
public:
void func(){
//a = 10;父类中的公共权限成员->子类中是私有权限
//b = 10;父类中的保护权限成员->子类中是私有权限
//c = 10; 父类中的私有权限成员,子类访问不到
}
};

4.6.3集成中的对象模型

问题:从父类继承过来的成员,哪些属于子类对象?

从父类继承下来的非静态成员属性都属于子类对象。其中私有成员只是被隐藏了,但是也还是会被继承下去。

4.6.4集成中的构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

先构造父类,再构造子类,析构的顺序与构造的顺序相反。

4.6.5继承同名函数处理方式

问题:当子类与父类出现同名成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Base{
public:
Base(){
this->a = 10;
}
void func(){
cout << "Base func" << endl;
}

//重载func()
void func(int x){
cout << "Base func" << endl;
}

public:
int a;

};

class Son:public Base{
public:
Son(){
this->a = 20;
}
void func(){
cout << "Son func" << endl;
}

public:
int a;
};

int main(){
Son s;
cout << s.a << endl;//调用子类的成员变量
cout << s.Base::a << endl;//调用父类同名成员变量

s.func();//直接调用子类函数
s.Base::func();//调用父类中同名函数
//如果子类中出现和父类同名的成员函数,子类的同名成员会隐蔽掉父类中所有同名成员函数
//如果想访问到父类中被隐藏的同名成员函数,需要加作用域
s.Base::func(100);
}

4.6.6继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员,需要加作用域

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)

4.6.7多继承语法

C++允许一个类继承多个类

语法:class 子类 : 继承方式 父类1,继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议多继承

总结:多继承中如果父类出现了同名的情况,子类使用时候要加作用域

4.6.8菱形继承

菱形继承概念:

两个派生类继承同一个基类

又有某个类同时继承两个派生类

这种继承被称为菱形继承或者钻石继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal{
public:
int age;
};

//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep:virtual public Animal{

};

class Camel:virtual public Animal{

};

class Alpaca:public Sheep,public Camel{

};

int main(){
Alpaca a;
//当菱形继承,两个父类拥有相同的数据,需要通过作用域加以区分
//a.Sheep::age = 18;
//a.Camel::age = 20;

//如果采用虚继承的方式,则age只有一份数据,不需要进行区分
cout << "a.age" << a.age << endl;
}

4.7多态

4.7.1多态的基本类型

多态是C++面向对象的三大特性之一

多态分为两类

  • 静态多态:函数重载运算符重载 属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
using namespace std;

class Animal{
public:
//虚函数
virtual void speak(){
cout << "Animal" << endl;
}
};

class Cat:public Animal{
public:
//子类重新实现父类虚函数后,就可以实现地址晚绑定了
void speak(){
cout << "Cat" << endl;
}
};

//地址早绑定,在编译阶段就确定了函数的地址
//如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行
//动态多态使用
//父类的指针或者引用 指向子类对象
void doSpeak(Animal &a){
a.speak();
}

int main(){
Cat c;
doSpeak(c);
}

重写:函数返回值类型 函数名 参数列表 完全相同

动态多态满足条件

  1. 有继承关系
  2. 子类要重写父类的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

4.7.2多态的底层原理

子函数的虚函数表内部会替换成子类的虚函数的地址

当父类的指针指向子类对象时候,发生多态

4.7.3纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 {参数列表} = 0

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base{
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//抽象类函数的特点:
//1、无法实例化对象
//2、抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};

class Son : public Base{
public:
virtual void func(){
cout << "func()函数调用" << endl;
}
}

int main(){

//抽象类是无法实例化对象的
//Base b; 错误
//new Base; 错误
//Son s; 子类必须重写父类中的纯虚函数,否则无法实例化对象

Base *base = new Son;
base->func();
}

4.7.4虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类析构代码

解决方式:将父类中的析构函数改为虚析构纯虚析构

虚析构和纯虚析构的共性:

  • 可以解决父类指针释放子类对象
  • 都需要具体的函数实现

虚析构和纯虚析构的区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:virual ~类名(){}

纯虚析构法:virtual ~类名() = 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <string>

using namespace std;

class Animal{
public:

Animal(){
cout << "Animal 构造函数调用" << endl;
}

//利用虚析构可以解决父类指针释放子类对象不干净的问题
virtual ~Animal(){
cout << "Animal 析构函数调用" << endl;
}
//纯虚析构,需要有声明,也需要有实现
//有了纯虚析构知乎,这个类也属于抽象类,无法实例化对象
//virtual ~Animal = 0;

//纯虚函数
virtual void speak() = 0;
};

class Cat:public Animal{
public:
Cat(string name){
this->name = new string(name);
}

virtual void speak(){
cout << "猫咪在说话" << endl;
}

~Cat(){
if(name != NULL){
cout << "Cat析构函数" << endl;
delete name;
name = NULL;
}
}

string *name;
};

int main(){
Animal * animal = new Cat("Tom");
animal->speak();
delete animal;
}

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  3. 拥有纯虚析构函数也属于抽象类

5 文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件进行操作需要包含头文件

文件类型分为两种:

  1. 文本文件:文件以文本的ASCII码形式储存在计算机文件当中
  2. 二进制文件:文件以文本二进制形式储存在计算机中,用户一般不直接读懂它们

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

5.1文本文件

5.1.1写文件

写文件步骤如下:

1.包含头文件:#include

2.创建流对象:ofstream ofs;

3.打开文件:ofs.open(“文件路径”,打开方式);

4.写数据:ofs << “写入的数据”;

5.关闭文件:ofs.close();

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意:文件打开方式可以配合使用,利用|操作符

例如:用二进制方式写文件ios::binary | ios::out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
//1、包含头文件
#include <fstream>

using namespace std;

int main(){
//2、创建该对象
ofstream ofs;
//3、指定打开方式
ofs.open("test.txt",ios::out);
//4、写内容
ofs << "Hello World" << endl;
ofs << "Its my introduction" << endl;
ofs << "Nice to meet you" << endl;
//5、关闭文件
ofs.close();
}

5.1.2读文件

读文件与写文件步骤相似,但是读取方式相对比较多

读取步骤如下:

  1. 包含头文件:#include <iostream>
  2. 创建对象流:ifstream ifs;
  3. 打开文件并判断文件是否打开成功:ifs.open("文件路径",打开方式);
  4. 读数据:四种读取方式
  5. 关闭文件:ifs.close
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
//1、包含头文件
#include <fstream>
#include <string>

using namespace std;

int main(){
//2、创建对象流
ifstream ifs;

//3、打开文件并判断文件是否打开成功
ifs.open("test.txt",ios::in);
if(!ifs.is_open()){
cout<< "文件打开失败" << endl;
}

//4、读数据的四种方法
char buf[1024] = {0};
while(ifs >> buf){
cout << buf <<endl;
}

// char buf[1024] = {0};
// while(ifs.getline(buf,sizeof(buf))){
// cout << buf << endl;
// }

// string buf;
// while(getline(ifs,buf)){
// cout << buf << endl;
// }

//char c;
//while((c = ifs.get()) != EOF){//EOF end of file
// cout << c;
//}

//5、关闭文件
ifs.close();
}

5.2二进制文件

以二进制方式对文件进行读写操作

打开方式要指定为:ios::binary

5.2.1写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型:ostream & write(const char * buffer ,int len)

参数解释:字符指针buffer指向内存中一段储存空间,len是读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

class Person{
public:
char name[64];
int age;
};

int main(){
ofstream ofs("person.txt",ios::out | ios::binary);
//ofs.open("person.txt",ios::out | ios::binary);
Person p = {"张三",18};
ofs.write((const char *)&p,sizeof(Person));

}

5.2.2读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream & read(char *buffer,int len);

参数解释:字符指针buffer指向内存中一段储存空间,len是读写字节数

1

C++提高编程

1模板

1.1模板的概念

模板就是建立通用的模具,大大提高复用性

模板的特点:

  • 模板不可以直接使用,它知识一个框架
  • 模板的通用并不是万能的

1.2函数模板

  • C++另一种编程思想称为泛型编程,主要利用的技术就是模板
  • C++提供两种模板机制:函数模板类模板

1.2.1函数模板语法

函数模板作用:

建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个虚拟类型来代表。

语法:

1
2
template<typename T>
函数声明或定义

解释:

template - 声明创建模板

typename - 表面其后面的符号是一种数据类型,可以用class代替

T - 通用数据类型,名称可以替换,通常为大写字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

template<typename T>
void myswap(T &a,T &b){
T tmp = a;
a = b;
b = tmp;
}

int main(){
int a = 10;
int b = 20;
//法一:直接调用
myswap(a,b);
cout << "a = " << a << endl
<< "b = " << b << endl;

double x = 1.1;
double y = 2.2;
//法二:指定参数调用
myswap<double>(x,y);
cout << "x = " << x << endl
<< "y = " << y << endl;
}

1.2.2函数模板注意事项

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
void swap(T &a,T &b){
int tmp = a;
a = b;
b = a;
}

template<typename T>
void fun(){
cout << "fun" << endl;
}

int main(){
int a = 10;
char b = 'b';
//swap(a,b); 错误,使用自动类型推导无法推导出一致的T类型

//fun(); 错误,无法确定出T

}

1.2.3函数模板案例

案例描述:

  • 利用函数模板封装一个排序函数,可以对不同数据类型数组进行排序
  • 排序则从大到小,排序算法为选择排序
  • 分别利用char数组和int数组进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

template<typename T>
void myswap(T &a,T &b){
T tmp = a;
a = b;
b = tmp;
}

template<typename T>
void selectSort(T p[],int length){
for(int i = 0;i < length - 1;i++){
int min = i;
for(int j = i + 1;j < length;j++){
if(p[min] > p[j]) min = j;
}
myswap(p[min],p[i]);
}
}

int main(){
//int p[] = {10,8,3,4,2,1};
char p[] = {'z','x','y','a','d'};
int length = sizeof(p)/sizeof(p[0]);

selectSort(p,length);

for(int i = 0;i < length;i++){
cout << p[i] << endl;
}
}

1.2.4普通函数与函数模板的区别

普通函数函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
void add(T a,T b){
return a + b;
}

int name(){
int a = 10;
char c = 'c';
//add(a,c); 错误,自动推导类型,无法进行类型转换
add<int>(a,c);//正确

}

总结:建议使用显示指定类型的方式调用函数模板,因为自己可以确定通用类型

1.2.5普通函数与函数模板的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void myPrint(int a,int b){
cout << "调用普通函数" << endl;
}

template<typename T>
void myPrint(T a,T b){
cout << "调用函数模板" << endl;
}

template<typename T>
void myPrint(T a,T b,T c){
cout << "调用函数模板的重载" << endl;
}

int main(){
int a = 10;
int b = 10;
myPrint(a,b);//1、调用普通函数
myPrint<>(a,b);//2、强制调用函数模板
myPrint<>(a,b,c);//3、调用重载后的函数模板
myPrint('a','b');//4、函数模板有更好的匹配,调用函数模板
}

1.2.6模板的局限性

局限性:

  • 模板的通用性并不是万能的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Person{
    public:
    string name;
    int age;
    }
    template<typename T>
    bool myCompare(T &a,T &b){
    if(a > b) return true;
    if(a < b) return false;
    }

    template<> bool myCompare(Person &p1,Person &p2){
    if(strcmp(p1.name,p2.name) == 0 && p1.age == p2.age){
    return true;
    }else
    return false;
    }

1.3类模板

1.3.1类模板基本语法

类模板作用:

  • 建立一个通用类,类中的成员数据类型可以不具体指定,用一个虚拟的类型来代表。

语法:

1
2
template<typename T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>

using namespace std;

template<class nametype,class agetype>
class Person{
public:
nametype name;
agetype age;

Person(nametype name,agetype age){
this->name = name;
this->age = age;
}

void show(){
cout << this->name << " " << this->age << endl;
}
}

int main(){
Person<string,int> p = new Person("Qingren",18);
p.show();
}

1.3.2类模板与函数模板区别

类模板与函数模板区别主要有两点:

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>

using namespace std;

template<class nametype,class agetype = int>
class Person{
public:
nametype name;
agetype age;

Person(nametype name,agetype age){
this->name = name;
this->age = age;
}

void show(){
cout << this->name << " " << this->age << endl;
}
}

int main(){
Person<string,int> p = new Person("Qingren",18);

//Person p = new Person("Qingren",18); 错误
Person<string> p = new Person();
}

1.3.3类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有去别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person1{
public:
void show1(){
cout << "调用Person1成员方法" << endl;
}
}

class Person2{
public:
void show2(){
cout << "调用Person2成员方法" << endl;
}
}

template<class T>
class MyClass{
T obj;
void test1(){
obj.show1();
}

void test2(){
obj.show2();
}
}

int main(){
Myclass<Person1> m;//这样编译是不会有问题的,只有我们在调用成员函数时,成员函数才会被创建
m.test1();//仅调用test1()也没有问题
//m.test2(); 如果test1和test2同时调用,就会出现错误

}

1.3.4类模板对象做函数参数

学习目标:

  • 类模板实例化出的对象,像函数传参的方式

一共有三种传入方式:

1.指定传入类型 —-直接显示对象的数据类型

2.参数模板化—-将对象中的参数变为模板进行传递

3.整个类模板化—-将这个对象类型模板化进行传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
template<class T1,class T2>
class Person(){

Person(T1 name,T2 age){
this->name = name;
this->age = age;
}

void showPerson(){
cout << "姓名:" << this->name << "年龄:" << this.age;
}

T1 name;
T2 age;
}

//1.指定传入的类型
void printPerson(Person<string,int>&p){
p.showPerson();
}

void test01(){
Person<string,int>p("悟空",100);

}

//2.参数模板化
template<class T1,class T2>
void printPerson2(Person<T1,T2>&p){
p2.showPerson2(p);
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}

void test02(){
Person<string,int>p2("八戒",90);
printPerson2(p2);
}

//3.整个类模板化
template<class T>
void printPerson3(T &p)
{
p3.showPerson();
cout << "T的数据类型" << typeid(T).name() << endl;
}

void test03(){
Person(string,int)p3("唐僧",30);
printPerson3(p3)
}

1.3.5类模板与继承

当类模板碰到继承时,需要注意以下几点:

  • 当子类继承的父类是一个模板时,子类在声明的时候,要指定父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活制定出父类中T的类型,子类也需变为类模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template<class T>
class Base
{
T m;
};

//class Son:public Base{ //这样继承是错误的,必须要知道父类中的T类型,才能继承给子类
class Son:public Base<int>
{


};

//如果想灵活指定父类中T类型,子类也需要改变类模板
template<class T1,class T2>
class Son2:public Base<T2>{
public:
Son2()
{
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
T1 obj;
};

int main(){

}

1.3.6类模板成员函数类外实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class T1,class T2>
class Person(){
public:
Person(T1 name,T2 age);

void showPerson();

T1 name;
T2 age;
}

template<class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age){
this->name = name;
this->age = age;
}

template<class T1,class T2>
void Person<T1,T2>::showPerson(){
cout << "姓名:" << this->name << "年龄" << this->age << endl;
}

1.3.7类模板分文件编写

存在一个问题:

  • 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决:

  • 方式1:直接包含.cpp源文件
  • 方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制

主流解决方法是第二种,在主函数文件中包含.hpp文件即可

1.3.8类模板与友元

全局函数类内实现 - 直接在类内声明友元即可

全局函数类内实现 - 需要提前让编译器知道全局函数的存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//类内实现
template<class T1,class T2>
class Person(){
//全局函数 类内实现
friend void printPerson(Person<T1,T2> p)
{
cout << "姓名:" << p.name << "年龄:" << p.age << endl;
}



public:
Person(T1 name,T2 age){
this->name = name;
this->age = age;
}

private:
T1 name;
T2 age;
}

void test01(){
Person<string,int>p("Tom",20);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//类外实现

//全局函数配合友元,要提前声明类模板
template<class T1,class T2> class Person;
//如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
void printPerson<>(Person<T1,T2> & p);

template<class T1,class T2>
void printPerson<>(Person<T1,T2> & p){
cout << "姓名:" << p.name << "年龄:" << p.age << endl;
}

template<class T1,class T2>
class Person(){
//全局函数 类内实现
friend void printPerson<>(Person<T1,T2> & p);

public:
Person(T1 name,T2 age){
this->name = name;
this->age = age;
}

private:
T1 name;
T2 age;
}

void test01(){
Person<string,int>p("Tom",20);
}

2 STL

  • C++面向对象泛型编程思想,目的就是复用性的提升
  • 大多数情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作
  • 为了简历数据结构和算法的一套标准,诞生了STL

2.1STL基本概念

  • STL(Standard Template Library,标准模板库)
  • STL从广义上分为:容器(container)算法(algorithm)迭代器(iterator)
  • 容器算法之间通过迭代器进行无缝连接
  • STL几乎所有的代码都采用了模板类或者模板函数

2.2STL六大组件

STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

  1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
  2. 算法:各种常用的算法,如sort、find、copy、for_each等
  3. 迭代器:扮演了容器与算法之间的胶合剂
  4. 仿函数:行为类似函数,可作为算法的某种策略
  5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西
  6. 空间配置器:负责空间的配置与管理

2.3 STL中容器、算法、迭代器

容器:STL容器就是将运用最广泛的一些数据结构实现出来

常用的数据结构:数组,链表,树,栈,队列,集合,映射表等

这些容器分为序列式容器关联式容器两种:

  • 序列式容器:强调值的排序,序列式容器中的每个元素均有固定位置
  • 关联式容器:二叉树的结构,各元素之间没有严格的物理上的顺序关系

算法:

有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)

算法分为:质变算法非质变算法

质变算法:是指运算过程中会更改去间内的元素内容。例如拷贝,替换,删除等等

非质变算法:是指运算过程中不会更改去间内的元素内容,例如查找,计数,遍历,寻找极值等等

迭代器:

提供一种方法,使之能够依序访问某个容器所含的各个元素。而无需暴露该容器的内部表示方式。

每个容器都有自己专属的迭代器

迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针

迭代器种类:

种类 功能 支持运算
输入迭代器 对数据的只读访问 只读,支持++、==、!=
输出迭代器 对数据的只写访问 只写,支持++
前向迭代器 读写操作,并能向前推进迭代器 读写,支持++、==、!=
双向迭代器 读写操作,并能向前和向后操作 读写,支持++、—
随机访问迭代器 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 读写,支持++、—、[n]、-n、<、<=、>、>=

常用的容器中迭代器种类为双向迭代器,和随机访问迭代器

2.4容器算法叠加器初识

2.4.1 Vector存放内置数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;
#include <vector>
#include <algorithm>

void Print(int val){
cout << val << endl;
}


int main(){
vector<int> v;

//向容器中插入数据
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);

//通过迭代器访问容器中的数据
vector<int>::iterator itBegin = v.begin();//起始迭代器 指向容器中第一个元素
vector<int>::iterator itEnd = v.end();//结束迭代器 指向容器中最后一个元素的下一个位置

//第一种遍历方法
// while(itBegin != itEnd){
// cout << *itBegin << endl;
// itBegin++;
// }

//第二种遍历方法
for_each(v.begin(),v.end(),Print);
}

2.4.2 Vector存放自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Person{
public:
string name;
int age;

Person(string name,int age){
this->age = age;
this->name = name;
}
};

void Print(Person *p){
cout << "姓名 = " << p->name << " 年龄 = " << p->age << endl;
}

int main(){
vector<Person*>v;

Person p1("aaa",10);
Person p2("bbb",20);
Person p3("ccc",30);
Person p4("ddd",40);
Person p5("ddd",50);

v.push_back(&p1);
v.push_back(&p2);
v.push_back(&p3);
v.push_back(&p4);
v.push_back(&p5);

// for_each(v.begin(),v.end(),Print);

for(vector<Person*>::iterator it = v.begin();it != v.end();it++){
cout << "姓名 = " << (*it)->name << " 年龄 = " << (*it)->age << endl;
}

}

2.4.3Vector容器嵌套容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main(){
vector<vector<int>> vv;
// cout << "Star" << endl;
vector<int> v1;
vector<int> v2;
vector<int> v3;
vector<int> v4;

for(int i = 0;i < 4; i++){
v1.push_back(i+1);
v2.push_back(i+1);
v3.push_back(i+1);
v4.push_back(i+1);
}

vv.push_back(v1);
vv.push_back(v2);
vv.push_back(v3);
vv.push_back(v4);

for(vector<vector<int>>::iterator it = vv.begin();it != vv.end();it++){
for(vector<int>::iterator vit = (*it).begin();vit != (*it).end();vit++){
cout << *vit << " ";
}
cout << endl;
}
}

3 STL常用容器

3.1string

3.1.1string基本概念

本质:

  • string是C++风格的字符串,而string本质是一个类

string和char*区别

  • char*是一个指针
  • string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器

特点

string类内部封装了很多成员方法

例如:查找find,拷贝copy,删除delete,替换replace,插入insert

string管理char*所分配的内存,不用担心赋值越界和取值越界等,由类内部进行负责

3.1.2string构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>

/*
string();//创建一个空字符串
string(const char* s);//使用字符串s初始化
string(const string& str);//使用一个string对象初始化另一个string对象
string(int n,char c);//使用n个字符串c初始化
*/

void test01(){
string s1;//默认构造

const char * str = "hello world";
string s2(str);//使用C语言风格字符串初始化

string s3(s2);//拷贝构造出s3

string s4(10,'a');//

}

3.1.3string的赋值操作

功能描述:

  • 给string字符串进行赋值

赋值函数原型:

  • string& operator=(const char s); //char类型字符串赋值给当前的字符串
  • string& operator=(const string &s); //把字符串s赋给当前的字符串
  • string& operator=(char c); //字符赋值给当前字符串
  • string& assign(const char *s); //把字符串s赋给当前字符串
  • string& assign(const char *s,int n); //把字符串s的前n个字符串赋给当前的字符串
  • string& assign(const string &s ); //把字符串s赋给当前字符串
  • string& assign(int n,char c); //用n个字符串c赋值给当前字符串

image-202010011745157253.1.4string字符串拼接

功能描述:

  • 实现在字符串末尾拼接字符串

函数原型:

  • string& operator+=(const char * str); //重载+=操作符
  • string& operator+=(const char c); //重载+=操作符
  • string& operator+=(const string &str); //重载+=操作符
  • string& append(const char *s); //把字符串s连接到当前字符串结尾
  • string& append(const char *s,int n); //把字符串s的前n个字符连接到当前字符串结尾
  • string& append(const string &s); //同operator+=(const string &str)
  • string& append(const string &s,int pos,int n) //字符串s中从pos开始的n个字符连接到字符串结尾

3.1.5字符串查找和替换

image-20201001174515725

  • find查找是从左往后,rfind从右往左
  • find找到字符串后返回查找的第一个字符位置,找不到返回-1
  • replace在替换时,要指定从哪个位置起,多少个字符,替换成什么样的字符串

3.1.6字符串的比较

功能描述:

  • 字符串之间的比较

比较方式:

  • 字符串比较是按字符的ASCII码进行对比

= 返回 0

>返回 1

\< 返回 -1

函数原型:

  • int compare(const string &s) const //与字符串s比较
  • int compare(const char *s) const; //与字符串s比较

3.1.7 string字符存取

string中单个字符存取方式有两种

  • char& operator[](int n); //通过[]方式存取字符

  • char& at(int n); //通过at方法获取字符

3.1.8 string插入和删除

  • string& insert(int pos,const char* s) //插入字符串
  • string& insert(int pos,const string& str) //插入字符串
  • string& insert(int pos,int n,char c); //在指定位置插入n个字符
  • string& erase(int pos,int n = npos); //删除从pos开始的n个字符

3.1.9 string子串

string substr(int pos = 0,int n = npos) const; //返回由pos开始的n个字符串组成的字符串

3.2 vector容器

3.2.1 vector基本概念

  • vector数据结构和数组非常相似,也称为单端数组
  • 不同之处在于数组是静态空间,而vector可以动态扩展
  • 动态扩展并不是在原空间之后续接新空间,而是找更大的内存空间,然后将元数据拷贝到新空间,释放原空间
  • vector容器的迭代器是支持随机访问的迭代器

3.2.2 vector构造函数

vector<T> v; //采用模板实现类实现,默认构造函数

vector(v.begin(),v.end()); //将v[begin(),end()]区间中的元素拷贝给本身

vector(n,elem); //构造函数将n个elem拷贝给本身

vector(const vector &vec); //拷贝构造函数

3.2.3 vector赋值操作

  • vector& operator=(cosnt vector &vec); //重载等号操作符
  • assign(beg,end); //将[beg,end) 前闭后开 区间中的数据拷贝赋值给本身
  • assign(n,elem); //将n个elem拷贝赋值给本身

3.2.4 vector容量和大小

  • empty(); //判断容器是否为空
  • capacity(); //容器的容量
  • size(); //返回容器中元素个数
  • resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新的位置,如果容器变短,则末尾超出容器长度的元素被删除
  • resize(int num,elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除

3.2.5 vector插入和删除

  • push_back(ele); //尾部插入元素ele
  • pop_back(); //删除最后一个元素
  • insert(const_iterator pos,ele); //迭代器指向位置pos插入元素ele
  • insert(const_iterator pos,int count,ele); //迭代器指向位置pos插入count个元素ele
  • erase(const_iterator pos); //删除迭代器指向的元素
  • erase(const_iterator start,const_itrator end);//删除迭代器从start到end之间的元素
  • clear(); //删除容器中的所有元素

3.2.6 vector数据存取

  • at(int idx); //返回索引idx指向的数据
  • operator[]; //返回索引idx指向的数据
  • front(); //返回容器中第一个数据元素
  • back(); //返回容器中最后一个数据元素

3.2.7 vector互换容器

  • swap(vec); //将vec与本身的元素互换

3.2.8 vector预留空间

  • reserve(int len); //容器预留len个元素长度,预留位置不初始化,元素不可访问

3.3 deque容器

3.3.1 deque容器基本概念

  • 双端数组,可以对头端进行插入删除操作

deque与vector区别:

  • vector对于头部的插入删除效率较低,数据量越大,效率越低
  • deque相对而言,对头部的插入删除速度会比vector快
  • vector访问元素时的速度会比deque快,这和两者内部实现有关

deque内部工作原理:

  • deque内部有一个中控器,维护每段缓冲区中的内容,缓冲区存放真实数据。
  • 中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间

  • deque容器的迭代器也是支持随机访问的

3.3.2 deque构造函数

  • deque<T> deq;
  • deque(beg,end); //将[beg,end)区间中的元素拷贝给本身
  • deque(n,elem); //将n个elem拷贝给本身
  • deque(const deque &deq); //拷贝构造函数

3.3.3 deque赋值操作

  • deque& operator=(const deque &deq); //重载登号操作
  • assign(beg,end); //将[beg,end)区间中的元素拷贝给本身
  • assign(n,elem); //将n个elem拷贝给本身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Test::test(){
cout << "test is ready" << endl;
deque<int> d1;
for(int i = 0;i < 10;i++){
d1.push_back(i);
}

deque<int> d2 = d1;

deque<int> d3;
d3.assign(d1.begin()+1,d1.end()-1);

deque<int> d4;
d4.assign(8,8);
}

3.3.4 deque大小操作

  • deque.empty(); //判断容器是否为空
  • deque.size(); //返回容器中元素的个数
  • deque.resize(num); //重新指定容器的长度为num,若容器变长,则默认填充新位置;若变短,则超出容器长度的元素将被删除
  • deque.resize(num,elem); //重新指定容器的长度为num,若容器变长,则用elem填充新位置;若变短,则超出容器长度的元素将被删除

3.3.5 deque插入和删除

两段的插入操作:

  • push_back(elem); //在容器尾部插入一个数据
  • push_front(elem); //在容器头部插入一个数据
  • pop_back(); //删除容器最后一个数据
  • pop_front(); //删除容器第一个数据

指定位置的操作:

  • insert(pos,elem); //在pos处插入elem元素的拷贝,返回数据的位置
  • inser(pos,n,elem); //在pos处插入n个elem数据,无返回值
  • inser(pos,beg,end); //在pos处插入[beg,end]区间的数据,无返回值
  • clear(); //清空容器所有数据
  • erase(beg,end); //删除[beg,end]区间的数据,返回下一个数据的位置
  • erase(pos); //删除pos数据,返回下一个数据的位置

注意事项

1 new出来的是指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Person{
public:
string name;
int age;

Person(string name,int age){
this->age = age;
this->name = name;
}
};

int main(){
Person *p = new Person("Qingren",18);//这里 new 出来的是一个指向存放在堆区数据的指针
Person r = Person("Yu",19);//这样叫显示初始化对象
Person q("Yao",20);//隐式初始化对象
cout << p->age << endl;
cout << (*p).name << endl;
}

2 容器对象.end()指针

容器对象.end()指针指向的是容器内数据的后一位,如果要访问容器中的最后一位数据:*(OB.end() -1 )

3 explicit 关键字

explicit 防止隐式地调用构造函数:

1
2
3
4
5
6
7
8
9
class A {
public:
explicit A(int a){
std::cout << a << std::endl;
}
}

A a(10); # 正确
A a = 10; # 错误