C++快速入门笔记
入门参考资料
分模块记录,每个模块会以问题的形式,检验是否掌握,内容都来自于入门资料
黑马的教程 case比较多,细节会稍微多一些;
C++基础
- 变量(数据类型,变量类型,作用域,左值右值)
- 常量
- 运算符
- 语句(判断,循环)
- 函数
- 修饰符类型,存储类
- 数组,字符串
- 指针,引用
- 输入输出流
- struct结构
变量
变量是什么?变量名是什么?
变量是一块
内存空间
;变量名就是这块内存空间的label数据类型的意义?C++提供了哪些内置的数据类型?
内存空间用来存储数据,数据有很多种,所以将数据划分为多种
数据类型
;
C++提供了7种基本内置数据类型
类型 关键字 布尔型 bool 字符型 char 整型 int 浮点型 float 双浮点型 double 无类型 void 宽字符型 wchar_t 变量类型是什么?变量定义?变量声明?变量初始化?
变量类型是变量(内存空间)存储的
数据类型
,所以变量类型和数据类型可以等价;
变量定义:type variable_list;
变量初始化:type variable_name = value;
变量声明:extern type variable_list; 只在编译链接的时候有意义,因为变量不能重定义,而其他文件又用到该文件的变量,需要声明变量;
变量定义只能一次,变量声明可以多次;什么是左值,什么是右值?
左值(lvalue):表示内存空间的表达式,可以出现在赋值运算符的左右两侧
右值(rvalue):表示内存空间里的值的表达式,只能出现在赋值运算的右侧什么是作用域,局部变量?全局变量?
作用域是程序的一块区域;
局部变量:函数或代码块内定义的变量;
全局变量:函数外部声明的变量变量,变量类型,数据,数据类型,内存空间 关系?
变量定义会开辟一块内存空间,所以变量 = 内存空间;
变量(内存空间)需要存储数据;
数据有不同的类型,不同的数据类型有不同的值;变量是数据的容器;所以变量有不同的变量类型,不同的变量类型存储不同的值;
常量
什么是常量?
常量是固定值,又叫做
字面量
常量就像是常规的变量,常量的值在定义后不能进行修改常见的有哪些常量?
整数常量,浮点常量,布尔常量,字符常量,字符串常量
如何定义常量?
- 宏定义 #define
- const关键字修饰
运算符
运算符有哪些?每种举一个例子
算术运算符:+ - * / …
关系运算符:> < …
逻辑运算符:&& || !
位运算符:& | ^ ~ << >>
赋值运算符: = += -= …
杂项运算符: sizeof , . -> condition ? x: y …什么是一元运算符,什么是二元运算符?
一元运算符只有一个操作数,二元运算符有两个运算符
C++中的运算符优先级是什么样的?
很难全部记住,但是可以着重记住几个常用的点
- 后缀 和 一元 最高;条件 赋值 逗号 最低
- 位运算 比 加减乘除 低
- 不确定就加(),()优先级最高
语句
什么是循环语句,有哪些类型?
循环语句:多次执行一个语句组
类型:while循环,for循环,do while 循环,嵌套循环什么是循环控制语句,有哪些类型?
循环控制语句,更改正常的循环流程
类型:break,continue,goto什么是判断语句,有哪些类型?
判断语句:指定条件,条件为true时执行语句,(false时可选执行语句)
类型:if;if else; 嵌套if;switch;嵌套switch条件运算符 和 if else的关系?
条件运算符可以代替 if else语句
函数
如何定义一个函数?
return_type function_name( parameter list )
{
body of the function
}函数定义需要有返回类型,函数名称,参数,函数主体
什么是函数声明,意义?
函数声明同样也是为了满足编译要求
不同文件:当一个文件调用函数(定义于另一个文件),需要函数声明,这样才可以编译;
同一文件:对于早期的编译器,同一文件中,程序调用之后才定义的函数,需要先在头部函数声明,满足编译要求;调用函数的过程?
程序调用函数时,控制权会交给被调用的函数,函数执行完任务后,控制权会交还给主程序;具体可以在汇编学习里的calling convention进一步了解
函数参数是什么?
调用函数的时候,有时会传递一些参数,调用的时候 这些参数称为 实际参数(实参);
函数内部,函数要使用这些参数,必须声明一些变量接收参数,这些变量称为形式参数(形参);什么是传参,有哪几种方式
传参就是 将实际参数 传递给 形式参数(实参 -> 形参)
传递的方式有三种- 传值(传值调用):实参的值复制给形参,不影响实参
- 传地址值(指针调用):实参的地址值复制给形参,影响实参
- 传引用(引用调用):实参被引用传递,形参是实参的引用,影响实参
1
2
3C++里上面两者统称为值传递(passed by value),函数被传值调用(called by value);
最后被称为引用传递(passed by reference),函数被引用调用(called by reference)
形参的类型决定了形参和实参交互的方式,形参是引用类型是引用传递,否则就是值传递lambda函数的意义,用法?
C++11的新特性,lambda函数是匿名函数,也叫lambda表达式
形式:
capture->return-type{body}
其中capture里捕获当前作用域的变量
常见用法:
[&] 所有外部变量隐式地以引用的方式捕获
[=] 外部变量隐式地以传值的方式捕获
修饰符类,存储类,枚举类
有哪些修饰符(修饰符类的值)
signed,unsigned,long,short
有哪些类型限定符(修饰符类的值)
const:限定的对象不能改变
volatile:限定的变量的值可以改变
restrict:C99特性有哪些存储类(存储类的值)
auto:局部变量的数据类型,C++11后称为关键字
register:定义变量存储在寄存器中
static:static修饰的局部变量在程序的生命周期内不销毁;static修饰的全局变量,作用域限制在声明的文件内
extern:在另一个文件中,声明全局变量或函数
mutable:适用于类的对象什么是枚举类,如何定义
枚举类是一种特殊的数据类型
enum enum-name { list of names } var-list;
enum-name是枚举类的名称,list of names是该枚举类的值1
2
3eg:
enum color { red, green, blue } c;
c = blue;
数组,字符串
数组是什么?如何声明,初始化?
数组是一种基本的数据结构,存储
相同类型元素的顺序集合
C++拥有 数组 数据结构声明数组:
type arrayName [ arraySize ];
,注意arraysize必须是大于0的常量,type是数组里元素的数据类型
初始化数组:type arrayName [ arraySize ] = {xxx, xxx, xxx};
什么是多维数组,如何声明,初始化?
多维数组,就是数组嵌套数组
声明:type name[size1][size2]...[sizeN];
数组名的实际意义?
数组名实际是数组首元素的地址(指针,指向数组首元素)
a[i] = *(a + i)如何传递数组给函数?函数如何返回数组
数组不能作为参数,所以只能传数组地址(指针,指向数组)
如果要传递一维数组,函数声明的时候形参有三种写法,但是编译器都是理解为接收一个指针1
(int *a), (int a[]), (int a[size])
数组也不能作为返回值,所以只能返回数组地址(指针,指向数组)
如果要返回一维数组,函数声明的时候,return type得是指针类型字符串如何表示?
字符串在C++里两种表示方式
- C风格字符串,本质是字符数组,末尾是null字符
- string类,C++标准库提供
指针,引用
什么是指针?
这个问题很考验理解
三个方面理解- 指针是变量,存储地址值
- 指针也是数据类型,值是地址值
- 指针也是地址,指向内存空间的值(内容)
指针基本运用
指针可以间接访问变量 *
指针可以用来传递数组等数据结构,传递给函数,函数返回
指针可以进行四种算术运算 ++ – + -引用是什么?
引用不是变量,引用只是变量的别名(变量的另一个变量名,另一个label)
引用的定义?
引用必须在创建的时候初始化,绑定到一个变量上,一旦绑定不能绑定其他的变量
引用作为参数?引用作为返回值?
引用可以作为参数,函数定义的时候,形参要声明引用
引用可以作为返回值,函数定义的时候,返回类型是引用类型
输入输出流
- C++标准库有哪些IO库?
iostream: cin, cout, cerr, clog,分别对用标准输入流,标准输出流,非缓冲标准错误流,缓冲标准错误流
iomanop: setw,setprecision,控制位数和精度
fstream: 文件输入输出流
struct 结构体
什么是struct,设计的意义是什么?
struct是用户自定义的数据类型;
因为数组只能存储相同数据类型的元素,而struct允许存储不同数据类型的元素如何定义struct
两种方式
- 直接定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct Books{
char title[50];
char author[50];
char subject[100];
int book_id;
}book;
定义struct类型的变量是 必须 struct Books book1;
2. 为创建的类型取一个别名
typedef struct
{
char title[50];
char author[50];
char subject[100];
int book_id;
}Books;
定义struct类型的变量是 Books book1;
- 直接定义
如何访问结构体成员
两种方式
- 定义struct类的变量,成员运算符(.)访问
- 定义指针,指向struct类变量,箭头运算符(->)访问
结构体作为函数参数
结构体作为函数参数,和其他类型的变量一样
可以传struct变量,或者传指针指向struct变量
C++面向对象
- 类和对象
- 成员
- 构造函数
- 析构函数
- 拷贝构造函数
- 静态成员
- 友元
- 内联
- 对象模型和this指针
- 面向对象三特性
- 封装
- 继承
- 多态
类和对象
类是什么?对象是什么?
两种方式理解
类的角度
类是用户自定义的数据类型
,它封装了数据和函数(成员)。有了类之后,类似声明基本类型的变量
一样,我们也可以声明类的变量(类的对象)
类的定义:定义一个类,就是定义一个数据类型的蓝图(也可以说定义 对象的蓝图),它定义了类的对象具有的成员(属性:类的对象包括了什么;行为:可以在这个对象上执行哪些操作),通过类创建出对象
对象的角度
对象的定义:C++中万物都是对象(所有的变量,我们都可以说是对象)对象具有属性和行为
具有相同性质的对象,我们就抽象为类
eg: 1 2 3 4 5 都是整数,我们抽象出一个int类,同理 猫,狗,鸟,鱼都是动物,我们抽象出一个animal类
类的成员是什么?
类中具有数据和函数,它们都是成员;
类中的数据称为成员变量(属性),函数称为成员函数(行为)类有哪些访问修饰符,对应的作用是什么
类有三种访问修饰符:
public 公共权限
protected 保护权限
private 私有权限作用:
public 类内可以访问 类外可以访问
protected 类内可以访问 类外不可以访问
private 类内可以访问 类外不可以访问在继承的时候,这三种权限也会有不同的继承方式
继承方式 基类的public成员 基类的protected成员 基类的private成员 继承引起的访问控制关系变化概括 public继承 仍为public成员 仍为protected成员 不可见 基类的非私有成员在子类的访问属性不变 protected继承 变为protected成员 变为protected成员 不可见 基类的非私有成员都为子类的保护成员 private继承 变为private成员 变为private成员 不可见 基类中的非私有成员都称为子类的私有成员 类的构造函数是什么?析构函数是什么?
背景:面向对象概念来源于生活,生活中对象一般需要
初始化和清理
,如果一个对象没有初始化,使用后果未知;如果一个对象使用完没有及时清理,会造成一定安全问题
C++利用构造函数 解决对象初始化的问题;析构函数解决对象清理的问题构造函数是一种
特殊的成员函数
,它会在每次创建对象时执行;作用:为某些成员变量设置初始值
析构函数是一种特殊的成员函数
,它会在每次销毁对象时执行;作用:对象销毁前,释放资源C++构造函数语法:类名(){} 可以有参数
C++析构函数语法:~类名(){} 不可以有参数构造函数分类,调用方式?
构造函数可以有参数,所以可以发生函数重载,我们可以分类 有参构造和无参构造
其中有参构造里有一种特殊的构造函数拷贝构造函数
;作用:将传入对象身上的所有属性,拷贝到新对象上构造函数的调用方式
无参构造函数调用 直接 class object
有参构造函数调用 分为以下三类:- 括号法 (最常用)
- 显示法 (利用匿名对象)
- 隐式法 (编译器转化)
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//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
// 构造函数调用
// 1. 括号法
Person p1; // 无参构造函数调用
Person p2(10) // 有参构造函数调用
Person p3(p2) // 拷贝构造函数调用
// 2. 显示法
Person p1; // 无参构造函数调用
Person p2 = Person(10); // 有参构造函数调用
Person p3 = Person(p2); // 拷贝构造函数调用
// 3. 隐式转化法
Person p1; // 无参构造函数调用
Person p2 = 10; // 转化 Person p2 = Person(10) 有参构造函数调用
Person p3 = p2; // 转化 Person p3 = Person(p2); 拷贝构造函数调用
// 注意1: Person p1(); 不是构造函数调用,这是一个函数声明
// 注意2: Person(10) 单独写就是匿名对象 当前行结束之后,马上析构
// 注意3: Person(p1) <==> Person p1; 不是构造函数调用,是对象声明什么是拷贝构造函数,拷贝构造函数什么时候会调用?
拷贝构造函数是一种
特殊的构造函数
,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
拷贝函数调用时机
- 使用一个已有对象来初始化一个新对象
- 复制对象,把它作为参数传递给函数
- 复制对象,并从函数返回这个对象
注意调用拷贝构造函数,赋值的区别
Person p2 = p1; // 隐式转化法,调用拷贝构造函数Person p2;
p2 = p1; //不是调用拷贝构造函数,赋值操作类声明的时候,编译器会默认提供哪些函数?构造函数的调用规则,可以简单讲一下吗?
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数 (无参,空实现)
2.默认析构函数 (无参,空实现)
3.默认拷贝构造函数 (值拷贝)构造函数调用规则:
首先明确三种构造函数;无参构造 有参构造 拷贝构造
如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数总结:如果用户定义某种构造函数,编译器就只会提供它之后的构造函数,前面的构造函数不再提供
C++中的初始化列表怎么操作?
初始化列表用于构造函数中,用来初始化属性
语法格式:
1
2
3
4
5
6
7
8
9//传统方式初始化
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) {}对象成员是什么?有对象成员的类 创建对象时的 构造顺序以及析构顺序?
类中成员是其他类对象时,我们称该成员为
对象成员
构造的顺序 :先调用对象成员的构造,再调用本类构造
析构顺序:与构造顺序相反静态成员是什么?
静态成员:在成员变量和成员函数前加上关键字static,称为静态成员
静态成员变量
性质- 所有对象共享同一份数据 (这意味着无论创建多少个类的对象,静态成员都只有一个副本)
- 在编译阶段分配内存 (静态成员存储在全局区)
- 类内声明,类外初始化 (静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。如果不加定义就会报错,初始化是赋一个初始值,而定义是分配内存)
静态成员函数
性质
普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 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
25class Person
{
public:
static int m_A; //静态成员变量 类内声明
static void func() //静态成员函数
{
m_A = 100;
}
}
int Person::m_A = 10; //静态成员变量 类外初始化
//静态成员 两种访问方式
// 1. 通过对象
Person p1;
p1.m_A = 100; //访问静态成员变量
p1.func(); //访问静态成员函数
// 2. 通过类名 + 范围解析运算符 ::
Person::m_A //访问静态成员变量
Person::func(); //访问静态成员函数成员变量和成员函数如何存储,空对象占内存吗?
只有非静态成员变量占对象空间(属于类的对象上);其余 成员函数(非静态,静态)和静态成员变量都不占对象空间;
空对象比较特殊,它占一个字节
原因:编译器会给每个空对象分配一个字节,为了区分空对象占内存的位置,每个空对象也有独一无二的内存地址;C++类中 this指针 有什么作用?它的用法?
首先我们要明确为什么要有this指针
成员函数只会有一份实例,所有对象会共用这个函数,那么函数内部如何区分是哪个对象调用自己
答案就是 用this指针 this指针指向调用对象 (也就是说 this指针存储了调用对象的地址:当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针)this指针的用途
- 解决形参和成员变量的命名冲突
- 返回对象本身,用于链式编程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Person
{
public:
Person(int age)
{
// 1. 当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
// 确保返回对象引用,而不是直接返回对象,直接返回对象是值拷贝,相当于创建了另一个副本,无法进行链式编程
Person& PersonAddPerson(Person p)
{
this->age += p.age;
// 2. *this 返回对象本身
return *this;
}
int age;
};
Person p1(10);
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); //链式编程思想 同一个对象多次操作空指针可以访问成员函数吗?
空指针可以访问成员函数;但是如果成员函数内部访问了成员,空指针就无法访问该成员函数
原因:任何对类成员的直接访问都被看作是 隐式引用了this指针,eg:当isbn类使用bookNo时,它隐式引用了this指针,就像我们书写了this->bookNo一样,所以空指针访问该函数会报错!
什么是友元,友元有哪些用法?
友元:程序里有些私有属性想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术;
作用:让一个函数或者类 访问另一个类中私有成员友元有三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
// 全局函数做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Building
{
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building);
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
};
void goodGay(Building * building)
{
cout << "好基友正在访问: " << building->m_SittingRoom << endl;
cout << "好基友正在访问: " << building->m_BedRoom << endl;
}// 类做友元
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
31class goodGay
{
public:
goodGay()
{
building = new Building;
};
void visit();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}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
38class Building
{
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
class goodGay
{
public:
goodGay()
{
building = new Building;
};
void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
void visit2();
private:
Building *building;
};
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void goodGay::visit2()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl; // 访问失败
}什么是指向类的指针?
指向类的指针 我一般称呼为 类的指针
常用术语:
假设有一个类a
a类的指针xxx,存储a类对象xxx的地址 或者说 指向a类对象xxx内联函数是什么,用法?
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方;
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用;
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 不是内联函数
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
//...
}
// 内联函数
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
//...
}定义在类声明之中的成员函数将自动地成为内联函数
1
2
3
4
5class A
{
public:
void Foo(int x, int y) { ... } // 自动地成为内联函数
}
面向对象三特性
封装是什么?封装的意义和作用?
封装是C++面向对象三大特性之一
封装:把数据和操作数据的函数捆绑在一起
作用:- 避免受到外界的干扰和误用,从而确保了安全
- 利于数据抽象(仅向用户暴露接口而把具体的实现细节隐藏起来的机制),将数据和函数加以权限控制
类的设计中,成员变量设为私有的好处
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性继承是什么?继承的意义和作用?
继承:根据一个已有的类来定义一个新类,新建的类继承了已有的类的成员,同时可以有自己的成员;
这个已有的类称为基类(父类),新建的类称为派生类(子类);继承则代表了 is a 关系继承的意义和作用:减少重复代码
语法:class 子类 : 继承方式 父类
继承方式有哪些,有什么不同?
继承方式一共有三种:
公共继承 public 公共权限
保护继承 protected 保护权限
私有继承 private 私有权限在继承的时候,不同的继承方式,派生类的权限不同
继承方式 基类的public成员 基类的protected成员 基类的private成员 继承引起的访问控制关系变化概括 public继承 仍为public成员 仍为protected成员 不可见 基类的非私有成员在子类的访问属性不变 protected继承 变为protected成员 变为protected成员 不可见 基类的非私有成员都为子类的保护成员 private继承 变为private成员 变为private成员 不可见 基类中的非私有成员都称为子类的私有成员 从父类继承过来的成员,哪些属于子类对象中?
父类中的所有非静态成员都会被子类继承下去;父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到;
子类继承父类后,当创建子类对象,也会调用父类的构造函数,父类和子类的构造和析构顺序是谁先谁后?
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反 (理解,现有父亲才能有儿子)
当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数因为有些人会认为子类和父类函数虽然同名,但是参数不同,可以直接访问父类成员函数,实际上不能
继承中同名的静态成员在子类对象上如何进行访问?
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
什么是多继承,多继承中如果父类有同名成员出现,子类对象如何访问?
多继承:一个子类可以有多个父类,它继承了多个父类的特性
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
如果多个父类之间有同名成员,子类对象使用这些成员时要加作用域
什么是菱形继承?菱形继承的问题是什么?解决方案?
两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承;
菱形继承问题:子类继承两份相同的数据,导致资源浪费以及毫无意义;
解决方案:利用虚继承利用虚继承后,SheepTuo的对象模型里有Animal类的m_Age,以及Sheep类的vbptr和Tuo类的vbptr,vbptr指向vbtable,vbtable里面记录了偏移量,最后找到Animal类的m_Age; 数据只有一份
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}什么是多态,使用条件?如何运用多态?
多态也是 面向对象特性之一;
多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态;多态意味着调用成员函数时,会根据调用对象的类型来执行不同的函数;静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址多态使用条件:有继承关系,子类重写父类中的虚函数
运用方式:父类指针或引用指向子类对象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
46class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
void DoSpeak(Animal & animal)
{
animal.speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}多态的对象模型?
多态场景里,父类会有一个虚函数指针vfptr,然后指向一个虚函数表vftable,表内记录了虚函数的地址;
子类会继承父类的虚函数指针和虚函数表,但是子类重写父类的虚函数时,子类中的虚函数表 内部 会覆盖成子类的虚函数地址什么是纯虚函数和抽象类?
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为
纯虚函数
;
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0;
当类中有了纯虚函数,这个类也称为
抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则子类也属于抽象类
什么是虚析构,纯虚析构?如何使用,为什么使用它们?
问题背景:
多态使用时,父类指针在释放时无法调用到子类的析构代码;
如果子类中有属性开辟到堆区,那么父类指针delete时无法调用子类的析构代码,会导致内存泄漏;解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构语法:
virtual ~类名(){}
纯虚析构语法:1
2
3
4// 类内
virtual ~类名() = 0;
// 类外
类名::~类名(){}`作用:虚析构或纯虚析构 通过父类指针释放子类对象
虚析构和纯虚析构共性:都需要有具体的函数实现
虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象注意:如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
C++泛型编程
- 函数模板
- 类模板
函数模板
什么是模板?
模板是泛型类或泛型函数的 蓝图;
模板是一种框架,针对不同的数据类型(泛型),实现相同的功能(函数模板,类模板)什么是函数模板?
函数模板:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表
语法:1
2template<typename 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
30
31
32
33
34
35
36
37
38
39// 交换 函数模板
template<class T>
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
//1. 自动类型推导
mySwap(a, b);
//2. 显示指定类型
mySwap<int>(a, b);
mySwap(a, c); // 错误,不能推导不出一致的T类型
}
// 函数模板
template<class T>
void func()
{
cout << "func 调用" << endl;
}
void test02()
{
//func(); //错误,不能确定出T的类型
func<int>(); //利用显示指定类型的方式,给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//普通函数
int myAdd01(int a, int b)
{
return a + b;
}
//函数模板
template<class T>
T myAdd02(T a, T b)
{
return a + b;
}
//使用函数模板时,如果用自动类型推导,不会发生隐式类型转换
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
cout << myAdd01(a, c) << endl; //正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99
//myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换
myAdd02<int>(a, 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
33
34
35
36
37
38
39
40//普通函数
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;
}
void test01()
{
//1、如果函数模板和普通函数都可以实现,优先调用普通函数
// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
int a = 10;
int b = 20;
myPrint(a, b); //调用普通函数
//2、可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); //调用函数模板
//3、函数模板也可以发生重载
int c = 30;
myPrint(a, b, c); //调用重载的函数模板
//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); //调用函数模板
}模板的通用性无法运用于
非内置数据类型
,如何解决?模板的通用性并不是万能的,无法作用域自定义数据类型
解决方案:具体化模板
作用:解决自定义类型的通用化
语法: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
40class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
string m_Name;
int m_Age;
};
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
if (a == b)
{
return true;
}
else
{
return false;
}
}
//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
if ( p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
{
return true;
}
else
{
return false;
}
}
类模板
什么是类模板?
类模板:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表;
语法: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
30template<class T>
类
//类模板
template<class NameType, class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
void test01()
{
// 指定NameType 为string类型,AgeType 为 int类型
Person<string, int>P1("孙悟空", 999);
P1.showPerson();
}类模板与函数模板区别?
- 类模板使用只能用显示指定类型方式
- 类模板中的模板参数列表可以有默认参数
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//类模板
template<class NameType, class AgeType = int> // 模板参数列表指定默认参数
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
//1. 类模板使用只能用显示指定类型方式
void test01()
{
// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
Person <string ,int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
p.showPerson();
}
//2. 类模板在模板参数列表中可以有默认参数
void test02()
{
Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
p.showPerson();
}类模板中成员函数创建时机?
普通类中的成员函数一开始就可以创建 (定义后就创建)
类模板中的成员函数在调用时才创建 (调用时创建)这里的一开始和调用,不是指编译阶段和运行阶段;
visual studio里的生成 其实就是 编译
类模板的对象,如何向函数传参?
三种传入方式:
- 指定传入的类型 — 直接显示对象的数据类型
- 参数模板化 — 将对象中的参数变为模板进行传递
- 类模板化 — 将这个对象类型模板化进行传递
推荐使用第一种,指定传入类型:直接显示对象的数据类型
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
36void printPerson1(Person<string, int> &p) //1. 指定传入的类型
{
p.showPerson();
}
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p) //2. 参数模板化
{
p.showPerson();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
Person <string, int >p("猪八戒", 90);
printPerson2(p);
}
template<class T>
void printPerson3(T & p) //3. 类模板化
{
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();
}
void test03()
{
Person <string, int >p("唐僧", 30);
printPerson3(p);
}类模板在继承的时候有什么注意事项?
如果父类是类模板,子类需要指定出父类中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
30
31template<class T>
class Base
{
T m;
};
//class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> //必须指定出父类中T的数据类型
{
};
void test01()
{
Son c;
}
//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2> // T2是char
{
public:
Son2()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
};
void test02()
{
Son2<int, char> child1;
}类模板成员函数分文件编写产生的问题以及解决方式?
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到;
解决方案:
- 将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
- 直接包含.cpp源文件 (不推荐)
类模板如何使用友元?
全局函数类内实现 - 直接在类内声明友元即可 // 推荐这一种
全局函数类外实现 - 需要提前让编译器知道全局函数的存在
C++ STL
- STL概念
- 常用容器
- string
- vector
- 算法
- 迭代器
STL概念
什么是STL,STL的设计背景?
STL:Standard Template Library,标准模板库;
设计背景:C++的面向对象和泛型编程思想,目的就是复用性的提升;为了建立数据结构和算法的一套标准,诞生了STL;STL大体有哪些内容,作用是什么?
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器、空间配置器
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
算法:各种常用的算法,如sort、find、copy、for_each等
迭代器:扮演了容器与算法之间的胶合剂。
仿函数:行为类似函数,可作为算法的某种策略。
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理。
容器
string容器的基本操作?
string是一个类,类内部封装了char*,以及很多函数;
构造函数
string(); //创建一个空的字符串 例如: string str;
string(const char* s); //使用字符串s初始化
string(const string& str); //使用一个string对象初始化另一个string对象
string(int n, char c); //使用n个字符c初始化赋值
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赋给当前字符串字符串拼接
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个字符连接到字符串结尾查找和替换
int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const; //查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const; //查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const; //从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串s字符串比较
int compare(const string &s) const; //与字符串s比较
int compare(const char *s) const; //与字符串s比较字符存取
char& operator[](int n); //通过[]方式取字符
char& at(int n); //通过at方法获取字符插入和删除
string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string& erase(int pos, int n = npos); //删除从Pos开始的n个字符子串
string substr(int pos = 0, int n = npos) const; //返回由pos开始的n个字符组成的字符串
vector容器的基本操作?
vector数据结构和数组非常相似,也称为单端数组;不同之处在于数组是静态空间,而vector可以动态扩展
动态扩展:并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间
构造函数
vectorv; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end()); //将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem); //构造函数将n个elem拷贝给本身。
vector(const vector &vec); //拷贝构造函数。赋值
vector& operator=(const vector &vec);//重载等号操作符
assign(beg, end); //将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem); //将n个elem拷贝赋值给本身。容量和大小
empty(); //判断容器是否为空
capacity(); //容器的容量
size(); //返回容器中元素的个数
resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除插入和删除
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_iterator end);//删除迭代器从start到end之间的元素
clear(); //删除容器中所有元素数据存取
at(int idx); //返回索引idx所指的数据
operator[]; //返回索引idx所指的数据
front(); //返回容器中第一个数据元素
back(); //返回容器中最后一个数据元素互换容器
swap(vec); // 将vec与本身的元素互换
vector(v).swap(v); //匿名对象,收缩内存 预留空间
reserve(int len); //容器预留len个元素长度,预留位置不初始化,元素不可访问。
// 减少vector在动态扩展容量时的扩展次数