C++基础入门
1 基础入门
1 项目创建
创建新项目–空项目–源文件右击–添加–新建项–C++文件
#include<iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
system("pause");
return 0;
}
2 注释
单行注释 “ //xxx ” 多行注释“ /xxx/ ”
3 变量
创建方式: 数据类型 变量名=变量初始值
4 常量
C++定义常量的两种方式
1、#define 宏常量 #define 常量名 常量值 注意:后不加分号
通常在文件上方定义,表示一个常量
2、const修饰的变量 const 数据类型 常量名=常量值;
在变量定义前加const,修饰该变量值为常量,不可修改
5 关键字
变量命名时不能使用关键字,避免引起歧义。
6 标识符命名规则
- 标识符不能是关键字
- 标识符只能由数字、字母、下滑线组成
- 标识符必须以字母或者下滑线开头
- 标识符字母区分大小写
2 数据类型
数据类型的意义:给变量分配合适的内存空间
7种基本数据类型
注意:没有string类型
类型 | 关键字 |
---|---|
布尔型 | bool |
字符型 | char |
整型 | int |
浮点型 | float |
双浮点型 | double |
无类型 | void |
宽字符型 | wchar_t |
1 不同数据类型占位和范围
类型 | 位 | 范围 |
---|---|---|
char | 1 个字节 | -128 到 127 或者 0 到 255 |
unsigned char | 1 个字节 | 0 到 255 |
signed char | 1 个字节 | -128 到 127 |
int | 4 个字节 | -2147483648 到 2147483647 |
unsigned int | 4 个字节 | 0 到 4294967295 |
signed int | 4 个字节 | -2147483648 到 2147483647 |
short int | 2 个字节 | -32768 到 32767 |
unsigned short int | 2 个字节 | 0 到 65,535 |
signed short int | 2 个字节 | -32768 到 32767 |
long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
signed long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long int | 8 个字节 | 0 到 18,446,744,073,709,551,615 |
float | 4 个字节 | 精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字) |
double | 8 个字节 | 双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字) |
long double | 16 个字节 | 长双精度型 16 个字节(128位)内存空间,可提供18-19位有效数字。 |
wchar_t | 2 或 4 个字节 | 1 个宽字符 |
默认情况下输出一位小数,会显示6位有效数字(有效数字:从左边起第一个不为零的数开始查)。
字符型:字符型型变量不是把字符本身放到内存中,而是把对应的ASCII码放到内存中。
可以用ASCII码对字符进行复制 char a=97;
2 sizeof关键字
作用:统计数据类型所占内存的大小
语法:sizeof(数据类型/变量)
3 转义字符
“\n” :换行 “两个反斜杠” :一个反斜杠
\t :空格:并不是指单独的一个空格,会和前面的字符组合起来满足8个字节
cout << "aaa\tbbb" << endl;
cout << "aaaa\tbbb" << endl;
cout << "aa\tbbb" << endl;
/*输出结果
aaa bbb
aaaa bbb
aa bbb
*/
4 字符串类型
1 C语言风格的字符串
char 变量名[] = “字符串值”;
2 C++风格的字符串
string 变量名=“字符串值”; 需要包含一个头文件#include
5 数据的输入
语法:cin >> 变量名
6 随机数
#include<iostream>
#include<stdlib.h>//使用"rand"需要此头文件
#include<ctime>//使用随机数种子需要此头文件
using namespace std;
//主函数
int main(){
//随机数种子
srand((unsigned int)time(NULL));//大写NULL
int random=rand() %41;//随机生成[0,40]
int random=rand() %41 +60;//随机生成[0+60,40+60]
return 0;
}
3 运算符
1 逻辑运算符
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 | (A && B) 为 false。 |
| | 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 | |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 | !(A && B) 为 true。 |
4 程序流程结构
分为顺序结构、选择结构和循环结构。
1 三目运算符
表达式1?表达式2:表达式3
表达式1为真则执行表达式2并返回执行结果,否则执行表达式3并返回执行结果。
2 switch语句
语法:
swicth(判断表达式)
{
case 结果1:表达式1;break;
case 结果2:表达式2;break;
case 结果3:表达式3;break;
default:表达式3;break;
}
- 判断表达式只能是字符型或者整型
- case里如果没有break,程序会自上而下一直执行
3 跳转语句
1、break语句
- 出现在switch条件语句中,终止case并跳出switch循环
- 出现在循环语句中,作用是跳出当前的循环语句
- 出现在嵌套循环中,跳出最近的内层循环语句
2、continue语句
跳过本次循环,执行下一次循环
3、goto语句
可以无条件跳转语句,如果标记的名称存在,执行goto时会直接跳转到标记位置。
语法: goto 标记;
cout << "a" << endl;
goto FLAG;
cout << "b" << endl;
FLAG:
cout << "c" << endl;
cout << "d" << endl;
5 数组
一个集合,其中存放了相同数据类型的元素
- 特点1:数组中数据的数据类型相同
- 特点2:数组是由连续的内存位置组成的
5.1 一维数组
5.1.1 一维数组的定义方式
- 数据类型 数组名[数组长度];
- 数据类型 数组名[数组长度]={值1,值2…};
- 数据类型 数组名[] = {值1,值2…};
注意:
-
定义了不初始化数组元素为随机值
-
==全部初始化,部分初始化,初始化空{ },==未赋值的元素为0
#include <stdio.h>
int main()
{
int a[2][4];
int b[2][4] = {1};
printf("a[1][0]==%d\n", a[1][0]); // 1091310544
printf("a[1][1]==%d\n", a[1][1]); // 455
printf("b[1][0]==%d\n", b[1][0]); // 0
printf("b[1][1]==%d\n", b[1][1]); // 0
}
注:数组名是常量,不可以进行再次赋值。
5.1.2 一维数组数组名称
作用:
- 可以统计整个数组所占内存空间。
- 可以获取数组在内存中的首地址。
#include <stdio.h>
int main()
{
int a[3];
int b[4];
printf("a===%p\n", a); // a===00000063081ff7b4
printf("b===%p\n", b); // b===00000063081ff7a0
printf("a size=%d\n", sizeof(a)); // a size=12
printf("b size=%d\n", sizeof(b)); // b size=16
}
5.2 二维数组
5.2.1 二维数组的定义方式
- 数据类型 数组名 [行数] [列数];
- 数据类型 数组名 [行数] [列数] = {{数据1,数据2},{数据3,数据4}};
- 数据类型 数组名 [行数] [列数] = {数据1,数据2,数据3,数据4};
- 数据类型 数组名 [ ] [列数] = {数据1,数据2,数据3,数据4};
定义二维数组时,如果初始化了数据,行数可以省略。从第一行开始依次排列
#include <stdio.h>
int main()
{
int a[][4] = {1, 2, 3, 4, 5};//先排1234第一行,再排第二行的5,其余默认为
printf("a[1][0]==%d\n", a[1][0]); // 5
printf("a[1][1]==%d\n", a[1][1]); // 0
}
5.2.2 二维数组数组名称
作用:
- 可以统计整个数组所占内存空间。
- 可以获取数组在内存中的首地址。
二维数组的首地址和其第一行数据的首地址相同,和第二行首地址不同。
5.3 字符数组的定义
以双引号赋值字符数组时,其数组长度比单引号赋值多1,用于存放 ‘\0’
#include <iostream>
using namespace std;
int main()
{
char a[] = {'a', 'b', 'c'};
char b[] = "abc";
cout << sizeof(a) << endl;//3
cout << sizeof(b) << endl;//4
}
6 函数
6.1 函数的定义
- 函数不能嵌套定义
返回值类型 函数名(参数列表)
{
函数体;
return表达式;
}
6.2 函数的声明
- 作用:告诉编译器函数名称以及如何调用。
- 如果不进行函数声明,该函数只能定义在mian函数的前面,否则无法调用。
- 函数可以多次声明,但是只能定义一次。
6.2.1 函数声明的语法
1、返回值类型 函数名(数据类型 形参,数据类型 形参);
2、返回值类型 函数名(数据类型,数据类型);
注意:在函数声明中,参数的名称并不重要,只有参数的类型是必需的
6.3 函数分文件的编写
作用:让代码的结构更加清晰
函数分文件编写流程:
1、在头文件目录中创建后缀为.h的头文件
2、在源文件目录中创建后缀为.cpp的源文件
3、在头文件中写函数的声明
#include<iostream>
using namespace std;
//函数声明
int sum(int a, int b);
4、在源文件中写函数的定义
#include"sum.h"//将源文件和头文件进行绑定
int sum(int a, int b)
{
return a + b;
}
5、调用此函数时,需要添加对应的头文件
#include<iostream>
#include "sum.h";
using namespace std;
int main ()
{
cout << "求和=" << sum(10, 20) << endl;
system("pause");
return 0;
}
7 指针
7.1 指针的基本概念
**作用:**可以通过指针简介访问内存
- 内存编号是从0开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址
- 指针只能保存开辟好的空间地址
7.2 指针的声明
**定义:**数据类型 *指针名
例: int *p;
可以认为 **数据类型 *** 为指针的数据类型,还有 char *;float *;double *
等
同时定义多个指针时每个指针名前都要加*
int main()
{
int a=10;
int *p;
//p存储的是变量的地址
//*p直接指向变量内容
p=&a;
cout<<p<<endl;//0x6ffe14
cout<<&a<<endl;//0x6ffe14
cout<<*p<<endl;//10
cout<<a<<endl;//10
return 0;
}
7.3 指针所占的内存空间
32位系统中指针占4个字节的空间,64位系统中指针占8个字节的空间。
32位系统中内存地址为0x0000 0000 到 0xffff ffff 。对于十六进制的数,一位4个二进制数,即4个bit,8位二进制数,32bit,4个字节
7.4 空指针和野指针
**空指针:**指针变量指向内存中编号为0的空间
**用途:**初始化指针变量
**注意:**空指针指向的内存是不可以访问的
**野指针:**指针变量指向非法的内存空间
空指针和野指针都不是我们申请的空间,因此不要访问,不能用于指针的初始化。
7.5 const修饰指针
const修饰指针有三种情况:
1、const修饰指针----常量指针。
const int *p;
指针的指向(内存地址)可以修改,但是指针指向的值(内存地址存储的值)不可以修改。
2、const修饰常量----指针常量
int * const p;
指针的指向不可以修改,指针指向的值可以修改。
3、const既修饰指针,又修饰常量
const int * const p;
7.6 指针和数组
**作用:**利用指针访问数组中的元素
- 数组名+下标
- 指针名+下标
- *(指针+n)
数组名是指向数组中第一个元素的常量指针(不能赋值,修改)。因此,在下面的声明中:
double runoobAarray[50];
runoobAarray 是一个指向 &runoobAarray[0] 的指针,即数组 runoobAarray 的第一个元素的地址。因此,下面的程序片段把 p 赋值为 runoobAarray 的第一个元素的地址:
double *p;
double runoobAarray[10];
p = runoobAarray;
使用数组名作为常量指针是合法的,反之亦然。因此,*(runoobAarray + 4) 是一种访问 runoobAarray[4] 数据的合法方式。
int main()
{
int arr [] = {2,5,7,9,12,30};
int *p=&arr[0];
cout<<*p<< endl;//2
p++;
cout<<*p<<endl;//5
cout<<"使用指针遍历数组"<<endl;
int *p1=arr;
for(int j=0;j<6;j++) {
cout<<*p1<<" ";
p1++;
}
return 0;
}
7.7 指针和函数
利用指针可以进行地址传递,和单纯的值传递不同,地址传递时函数体内部的操作可以改变原始参数的值。
8 结构体
8.1 结构体基本概念
结构体是用户自定义的数据类型,允许用户存储不同的数据类型。
8.2 结构体的定义和使用
语法: struct 结构体名称{结构体成员1;结构体成员2;…};
注意:结构体定义最后有分号。
通过结构体创建变量的方式有3种:
创建变量时struct关键字可以省略
- struct 结构体名称 变量名;
- struct 结构体名称 变量名 = {成员1,成员2,…};
- 定义结构体的同时创建变量;
#include<iostream>
#include<string>
using namespace std;
//定义结构体
struct Student
{
int id;
string name;
int age;
}s3;
//主函数
int main()
{
//创建结构体01
Student s1;
s1.id=001;
s1.name="李四";
s1.age=22;
cout << s1.id <<" "<< s1.name<<" "<< s1.age <<endl;
//创建结构体02
Student s2={002,"张三",18};
cout << s2.id <<" "<< s2.name<<" "<< s2.age <<endl;
//创建结构体03--------在结构体定义的末尾创建变量
s3.id=003;
s3.name="王五";
s3.age=55;
cout << s3.id <<" "<< s3.name<<" "<< s3.age <<endl;
return 0;
}
8.3 结构体数组
**作用:**将自定义的结构体放入数组中方便管理
**语法:**struct 结构体名称 数组名[元素个数] = {{},{},{},…};
8.4 结构体指针
**作用:**通过指针访问结构体中的内容。
- 利用操作符 -> 可以通过结构体指针访问结构体属性。
//创建结构体
Student s2={002,"张三",18};
//普通方式访问
cout << s2.id <<" "<< s2.name<<" "<< s2.age <<endl;
//指针方式访问
Student *p=&s2;
cout << p->id <<" "<< p->name<<" "<< p->age <<endl;
8.5 结构体嵌套结构体
8.6 结构体作为函数参数
将结构体作为参数向函数中传递,传递方式有两种:值传递和地址传递。
8.7 结构体中const的使用场景
**作用:**使用const来防止误操作
进行值传递时先进行复制操作,当数据量很多时,会造成资源浪费。但是使用地址传递时,会存在变量被误修改的情况,此时需要使用const来修饰。
在函数的参数列表里使用const修饰。
C++核心编程
主要针对面向对象编程技术做详细讲解。
1 内存分区模型
C++程序执行时将内存划分为4个区域:
- 代码区,存放函数体的二进制代码,由操作系统进行管理
- 全局区,存放全局变量、静态变量、常量
- 栈区,由编译器自动分配释放,存放函数的参数值、局部变量等。
- 堆区,由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
**内存分区的意义:**不同区域存放的数据赋予不同的生命周期,给编程提供更大的灵活性。
1.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了其指令
全局区:
内容:全局变量,静态变量,常量(字符串常量和全局常量)
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和全局常量也存放于此,局部常量不存在全局区。
- 该区域的数据在程序结束后由操作系统释放
1.2 程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等
**注意事项:**不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
- 由程序员分配释放,如果程序员不释放,程序结束后由系统自动回收
- C++中主要利用new在堆区中开辟内存
1.3 new操作符
C++中使用new操作符在堆区开辟数据
堆区开辟的数据由程序员手动开辟,手动释放,释放利用操作符delete
语法:new 数据类型
int *p=new int(10); //10为初始值
delete p;//普通变量内存释放方式
int *arr=new int[10];//10为数组长度
delete[] arr;//数组内存释放方式
利用new创建的数据会返回该数据对应类型的指针
2 引用
2.1 引用的基本概念
**作用:**给变量起别名
**语法:**数据类型 &别名 = 原名;
2.2 引用的注意事项
- 引用必须初始化
- 引用在初始化后不可以修改
2.3 引用做函数参数
**作用:**函数传参时,可以利用引用的技术让形参修饰实参
**优点:**可以简化指针修改实参
int main()
{
int a=10;
int b=20;
mySwap(a,b);
cout << "a=" << a <<";b=" << b << endl;// 20 10
}
//传递引用参数实现 值交换函数
void mySwap(int &a, int &b) {
int temp=a;
a=b;
b=temp;
}
2.4 引用做函数的返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
2.5 引用的本质
在C++中引用的本质是一个指针常量
引用内部其实存储了一个地址,因此引用在函数中传递时函数可以修改实参的值。
int a=10;
int &b=a;//相当于int * const b=&a;因为是指针常量,引用指向的地址不可以再次修改
b=20;//识别为引用,自动使用解引用*b=20;
2.6 常量引用
**作用:**常量引用主要用来修饰形参,防止误操作。在函数形参列表中可以加const修饰形参,防止形参改变实参。
void doPrint(const int &a){
a=20;//报错,这里不能修改
}
3 函数提高
3.1 函数默认参数
在C++中,函数的形参列表中的参数是可以有默认值的
**语法:**返回值类型 函数名 (参数=默认值){}
注意
- 对于多个参数,如果一个参数有了默认值,后边的所有参数都必须有默认值
- 如果函数的声明有默认值,函数的定义就不可以有默认值
int doSum(int a=10, int b=20,int c=30){
return a+b+c;
}
int main()
{
cout << doSum() << endl;//60
cout << doSum(0) << endl;//50
}
3.2 函数占位参数
C++中函数的形参列表里可以有占位参数来占位,调用函数时必须填补该位置
**语法:**返回值类型 函数名 (数据类型){};
int doSum(int a,int){// int doSum(int a,int =30){ 占位参数可以有默认值
cout << "doSum执行" << endl;
}
int main()
{
doSum(10,20);//doSum执行
}
3.3 函数重载
3.3.1 函数重载概述
作用:函数名可以相同,提高函数的复用性
函数重载条件:
- 同一个作用域下
- 函数名相同
- 函数参数不同:类型不同,个数不同,顺序不同
函数的返回值不同不可以作为函数重载的条件。
void doSum(){
cout << "doSum执行--无参数" << endl;
}
void doSum(int a){
cout << "doSum执行--一个整型" << endl;
}
void doSum(int a,int b){
cout << "doSum执行--两个整型" << endl;
}
void doSum(int a,char s){
cout << "doSum执行--一个整型一个字符型" << endl;
}
int main()
{
doSum();//doSum执行--无参数
doSum(12);//doSum执行--一个整型
doSum(10,20);//doSum执行--两个整型
doSum(12,'a'); //doSum执行--两个整型
}
3.3.2 函数重载注意事项
- 引用作为重载条件
void doSum(int &a){
cout << "doSum执行--无const修饰" << endl;
}
void doSum(const int &a){
cout << "doSum执行--const修饰" << endl;
}
int main()
{
int a=10;
doSum(a);//doSum执行--无const修饰
//int &a=10;不合法
//const int &a=10; 合法 相当于 int b=10; int &a=b;
doSum(10);//doSum执行--const修饰
}
- 函数重载遇到默认参数
#include<iostream>
using namespace std;
void doSum(int a){
cout << "doSum执行--一个参数" << endl;
}
void doSum(int a,int b=10){
cout << "doSum执行--一个参数+一个默认参数" << endl;
}
int main()
{
int a=10;
doSum(a);//程序执行报错,因为两个重载的函数都可以合法的执行
}
**自己的理解:**函数重载满足的条件是传递的参数值只能满足函数调用重载的一个函数。
4 类和对象
C++面向对象3大特征:封装、继承、多态
4.1 封装
4.1.1 封装的意义
- 将属性和行为作为一个整体来表现事物
- 为属性和行为添加权限控制
创建类的语法:class 类名{控制权限:属性/行为};
const double PI=3.14;
//圆形类的创建
class Circle
{
public:
int length;
double getLength()
{
return 2*PI*length;
}
};
//主函数
int main()
{
Circle c1;//创建圆对象
c1.length=20;//设置圆半径
cout << c1.getLength() <<endl;
}
4.1.2 封装中的权限
- public 公共权限,类内可以访问,类外可以访问
- protected 保护权限,类内可以访问,类外不可以访问,子类可以访问父类
- private 私有权限,类内可以访问,类外不可以访问,子类不可以访问父类
4.1.3 struct和class的区别
在C++中struct和class的唯一区别就在于默认的访问权限不同
- struct默认公共访问权限
- class默认私有权限
//圆形类的创建
class Circle
{
int length;
};
//圆形结构体的创建
struct circle
{
int length;
};
//主函数
int main()
{
Circle c1;//创建圆对象
c1.length=10; //报错,int circle:length is private
struct circle c2;//创建圆结构体
c2.length=20;
}
4.1.4 成员属性设置为私有
优点:
- 将所有成员属性设置为私有可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
4.1.5 将类作为文件独立出来
和函数类似,需要编写头文件和源文件。
头文件类似于接口,定义属性和函数声明。
源文件负责函数的实现,需要在函数前添加作用域。
以point类为例,分别编写point头文件和point源文件
point头文件
#pragma once //防止头文件重复包含
#include<iostream>
using namespace std;
class Point
{
private:
double x;
double y;
public:
void setX(double x1);
void setY(double y1);
double getX();
double getY();
};
point源文件
#include "point.h"//包含对应的头文件
//只写函数的实现,函数名称前加作用域
void Point::setX(double x1)
{
x = x1;
}
void Point::setY(double y1)
{
y = y1;
}
double Point::getX()
{
return x;
}
double Point::getY()
{
return y;
}
4.2 对象的初始化和清理
- 一个对象或者变量未初始化对其进行使用,结果未知
- 使用完一个对象或者变量不进行清理会造成一定的安全问题
C++利用构造函数和析构函数解决对象的初始化和清理问题,这两个函数会被编译器自动调用完成对象的初始化和清理工作,对象的初始化和清理工作是编译器强制要求我们做的事情,因此如果不手动提供构造和析构函数,编译器会提供,此时的构造函数和析构函数是空实现。
- **构造函数:**在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
- **析构函数:**在对象销毁前系统自动调用,执行清理工作。
4.2.1 构造函数语法
类名(){}
- 构造函数没有返回值,也不写void
- 函数名称和类名相同
- 构造函数可以有参数,因此可以发生重载
- 创建对象时会自动调用构造函数,无需手动调用,并且只会调用一次
4.2.2 析构函数语法
~类名(){}
- 析构函数没有返回值也不写void
- 函数名称和类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不能发生重载
- 程序在对象销毁前会自动调用析构函数,无需手动调用,并且只会调用一次
4.2.3 构造函数的分类及调用
2种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
注意事项:
- 使用默认无参构造时,不要加括号,会被认为是函数声明。
3种调用方式:
- 括号法
- 显示法
- 隐式转换法
注意事项:
- Person(“李四”,10010); 这是一个匿名对象,当前行执行结束后系统会立即回收掉该对象
- 不要利用拷贝构造函数初始化匿名对象,这会造成重定义。Person(p1); 这个语句相当于Person p1,相当于重新创建了一个名为p1的变量,造成了重定义
- 隐式转换法好像只对单参数的构造方法有效?多参数的使用时报错。
#include<iostream>
using namespace std;
class Person
{
public:
//无参构造函数
Person()
{
}
//有参构造函数
Person(string name,int id)
{
pName=name;
id=pId;
}
//拷贝构造函数
Person(const Person &p)
{
pName=p.pName;
}
string pName;
int pId;
};
int main()
{
//自动调用无参构造函数
Person p1;
//括号法调用有参构造函数
Person p2("张三",12138);
//括号法调用拷贝构造函数
Person p3(p2);
//显示法调用有参构造函数
Person p4=Person("李四",10010);
//显示法调用拷贝构造函数
Person p5=Person(p4);
//这是一个匿名对象,当前行执行结束后系统会立即回收掉该对象
Person("李四",10010);
//不要利用拷贝构造函数初始化匿名对象,这会造成重定义
//Person(p5); 这个语句相当于Person p5,相当于重新创建了一个名为p5的变量,造成了重定义
//隐式转换法 单参数时才能使用?
//Person p6= ("王五");
cout << p1.pName << endl;
cout << p2.pName << endl;
cout << p3.pName << endl;
cout << p4.pName << endl;
cout << p5.pName << endl;
cout << p6.pName << endl;
}
4.2.4 拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常有3种情况:
-
使用一个已经创建完毕的对象来初始化一个新的对象
-
值传递的方式给函数参数传值
因为是值传递,会创建出新的副本就会调用拷贝构造函数。
-
以值方式返回局部对象
class Person
{
public:
//无参构造函数
Person()
{
}
//有参构造函数
Person(int age)
{
pAge = age;
}
//拷贝构造函数
Person(const Person& p)
{
pAge = p.pAge;
}
int pAge;
};
//使用一个已经创建完毕的对象来初始化一个新的对象
void test01()
{
Person p1(10);
Person p2(p1);
cout << p1.pAge << " " << p2.pAge << endl;
}
//值传递的方式给函数参数传值
//因为是值传递,会把原来的对象拷贝一份,函数的操作不会对原来的对象产生改变
void test02(Person p)
{
p.pAge = 100;
cout << "test02执行" << endl;
}
//值方式返回局部对象
//因为是局部对象,函数执行后会被销毁,不能直接返回局部对象,而是拷贝一份返回
Person test03()
{
Person p(20);
cout << (int*)&p << endl;
return p;
}
int main()
{
test01();
Person p3(120);
test02(p3);
cout << p3.pAge << endl;
cout << &p3 << endl;
Person p4 = test03();
cout << (int*)&p4 << endl;//这个地址和test03中p的地址不一样,说明已经不是同一个对象
}
4.2.5 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数,无参,函数体为空
- 默认析构函数,无参,函数体为空
- 默认拷贝构造函数,对属性值进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认的无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
4.2.6 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
#include<iostream>
using namespace std;
class Person
{
public:
//无参构造函数
Person()
{
}
//有参构造函数
Person(int age,int hight)
{
pAge = age;
//这里有参构造创建出的hight对象存储在堆区,需要手动释放内存
pHight = new int(hight);
}
//深拷贝实现拷贝构造函数
Person(const Person& p)
{
pAge = p.pAge;
pHight = new int(*p.pHight);
}
//析构函数
~Person()
{
cout << "析构函数执行" << endl;
//手动释放堆区的内存
if (pHight != NULL)
{
delete pHight;
pHight = NULL;
}
}
int pAge;
int* pHight;
};
int main()
{
Person p1(12,180);
cout << p1.pAge << " " << *p1.pHight << endl;
Person p2(p1);
cout << p2.pAge << " " << *p2.pHight << endl;
}
默认拷贝构造函数是值传递,使用浅拷贝。会出现非法操作使程序中断。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K06NM5Ma-1678887322679)(C:\Users\wl\Desktop\C++核心编程.assets\image-20220616172218017.png)]
使用深拷贝定义拷贝构造函数,解决重复释放堆内存的问题。
**总结:**如果属性有在堆区开辟的一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
4.2.7 初始化列表
**语法:**构造函数():属性1(值1),属性2(值2)…{}
Person(int a,int b,int c) : m_a(12),m_b(11),m_c(13)
{
}
调用构造函数时,传参就按照参数值进行赋值,不传参就赋默认值
4.2.8 类对象作为类成员
C++中的成员可以是其他类的对象,称之为 对象成员。
**构造时:**成员对象先构造
**销毁时:**主体对象先析构
4.2.9 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,成为静态成员
静态成员的分类:
- 静态成员变量
- 所有对象共享同一份数据,这个类创建出的所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态变量访问方式
- 对象.静态变量
- 类名::静态变量
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
C++中类的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
- 空对象占据一个字节
class Person
{
};
Person p;//p占据1个字节
- 空对象中定义非静态变量int,占据4个字节
class Person
{
int a;
};
Person p;//p占据4个字节
- 空对象中定义非静态变量,静态变量和函数,仍占据4个字节,只有非静态成员变量跟随类的对象存储
class Person
{
int a;
static int b;
void func(){}
};
Person p;//p占据4个字节
4.3.2 this指针概念
每个非静态成员函数只会诞生一份函数实例,对于此类创建的多个对象来说,它们会共用此函数代码,那么这块代码是怎么区分是哪个对象调用自己的呢?
C++提供了特殊的对象指针,this指针解决上述问题。this指针指向被调用成员函数所属的对象
- this指针是隐含每一个非静态成员函数的指针
- this指针不需要定义,可直接使用
this指针的用途:
- 当形参和成员变量同名时,可用this指针进行区分
- 在类的非静态成员函数中返回对象本身时,可使用return *this
class Person
{
public:
string name;
Person(string name)
{
this->name = name;//利用指针访问变量时用的是-> 或者(*this).name=name;
}
};
返回*this时返回引用和返回对象的区别
class Person
{
public:
int age;
Person(int age)
{
this->age = age;
}
//返回Person对象
Person addAge(Person& p)
{
this->age += p.age;
return *this;
}
//返回引用
Person& addAge1(Person& p)
{
this->age += p.age;
return *this;
}
};
int main()
{
Person p1(10);
Person p2(10);
//返回对象时主函数得到的Person是用拷贝构造函数创建的,是一个新的Person对象,
//每返回一次得到一个新的对象和p1已经没有关系了
p1.addAge(p2).addAge(p2).addAge(p2);//20
//返回引用时,每次返回的都是p1
p1.addAge1(p2).addAge1(p2).addAge1(p2);//40
cout << p1.age << endl;
}
4.3.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,比如函数只是输出一段话,但是要注意有没有用到this指针.
如果用到this指针(访问对象的属性就会用到this,因为访问时前边默认带this->),需要加以判断保证代码的健壮性。
4.3.4 const修饰成员函数
常函数:
- 成员函数后(小括号后,大括号前)加const后称之为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
对于Person类中的成员函数而言,其中的this其实是Person* const this,指针常量,因此this的指向不可以修改,如果想让this指向的值也不能修改,可以改为const Person* const this,常函数所加的const就是第一个const
class Person
{
public:
//常函数
void showName() const
{
}
}
常对象
- 声明对象前加const称该对象为常对象
- 常对象只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)
4.4 友元
类中有些私有属性也需要让类外的一些特殊函数和特殊类进行访问,需要用到友元技术
友元的关键字:friend
友元的3种实现:
- 全局函数做友元
- 在类体内添加 friend + 函数声明;
- 类做友元
- 在类体内添加 friend + class + 类名;
- 成员函数做友元
- 在类体内添加 friend + 函数声明;(这里需要在函数名称前加上函数的作用域 类名:: )
#include <iostream>
using namespace std;
class Building
{
//全局函数做友元,在类体内添加 friend+函数声明;
friend void goodFriend(Building& b);
public:
string settingroom;
Building(string settingroom,string bedroom)
{
this->settingroom = settingroom;
this->bedroom = bedroom;
}
private:
string bedroom;
};
void goodFriend(Building& b) {
cout << "访问" << b.settingroom << endl;
//不进行友元的添加,此函数不能访问Building类的私有属性
cout << "访问" << b.bedroom << endl;
}
int main(){
Building b("客厅", "卧室");
goodFriend(b);
system("pause");
return 0;
}
4.5 运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能以适应不同的数据类型
4.5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
- 成员函数重载加号运算符
- 全局函数重载加号运算符
class Person
{
public:
int a;
int b;
Person() {};
Person(int a,int b)
{
this->a = a;
this->b = b;
}
//1、通过成员函数实现 + 运算符重载
//Person operator+ (Person& p)
//{
// Person temp;
// temp.a=this->a + p.a;
// temp.b=this->b + p.b;
// return temp;
//}
};
//通过对 + 进行重载,满足两个Person对象相加时将AB属性相加得到新的Person对象
//2、通过全局函数实现 + 运算符重载
Person operator+ (Person& p1,Person& p2)
{
Person temp;
temp.a = p1.a + p2.a;
temp.b = p1.b + p2.b;
return temp;
}
int main(){
Person p1(10, 20);
Person p2(20, 20);
Person p3 = p1 + p2;
cout <<"p3的a属性为:" << p3.a <<";p3的b属性为:" << p3.b << endl;
system("pause");
return 0;
}
4.5.2 左移运算符重载
作用:可以输出自定义数据类型
class Person
{
friend ostream& operator<< (ostream& cout, Person& p);
int a;
int b;
public:
Person() {}
Person(int a,int b)
{
this->a = a;
this->b = b;
}
//成员函数重载左移运算符不合适,不能实现cout在左,对象在右
};
ostream& operator<< (ostream &cout,Person& p)
{
cout << "p.a=" << p.a << ";p.b=" << p.b;
return cout;
}
int main(){
Person p1(12, 20);
cout << p1 <<endl;
system("pause");
return 0;
}
4.5.3 递增运算符重载
作用:通过重载递增运算符可以实现自己的整型数据
这里有一个问题:
ostream& operator<<(ostream& cout, Myint m);重载左移运算符
当参数为Myint m时,p++和++p都可以正常输出
当参数为Myint& m时,cout<< ++p正常,cout <<p++ 会报错,为什么?
是因为前置重载返回的引用,后置重载返回的对象?
//实现i++和++i的重载,以成员函数形式
class Myint
{
friend ostream& operator<<(ostream& cout, Myint m);
private:
int age;
public:
Myint(int age)
{
this->age = age;
}
//前置运算符重载
//1、返回void不能和左移运算符匹配
//2、返回Myint的引用可以实现链式++
//3、Myint& operator++() 默认为前置++
Myint& operator++()
{
age++;
return *this;
}
//后置运算符重载
//参数添加int作为占位参数,区分前置++
//这里如果返回Myint的引用,temp时是局部变量,函数结束后会被销毁,会造成非法操作,只能返回Myint
Myint operator++(int)
{
Myint temp=*this;
age++;
return temp;
}
};
//左移运算符重载
ostream& operator<<(ostream& cout, Myint m)
{
cout << m.age ;
return cout;
}
void test01()
{
Myint p(11);
cout << ++p << endl;
}
void test02()
{
Myint p(12);
cout << p++ << endl;
}
int main(){
test01();
test02();
system("pause");
return 0;
}
4.5.4 赋值运算符重载
C++编译器至少为一个类添加4个函数
- 默认构造函数,无参,函数体为空
- 默认析构函数,无参,函数体为空
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时会出现深浅拷贝的问题。
class Person
{
public:
int* age;
Person() {}
Person(int age)
{
//age属性在堆中开辟,普通的赋值是值传递,浅拷贝
this->age = new int(age);
}
~Person()
{
if (age != NULL)
{
delete age;
age = NULL;
}
}
//如果要使用连续赋值a=b=c这种,必须返回Person才行,否则就会出现b=c;a=void;的情况,报错
Person& operator=(Person& p)
{
this->age = new int(*p.age);
return *this;
}
};
int main() {
Person p1(20);
Person p2;
p2 = p1;
cout << "p1.age=" << *p1.age << endl;
cout << "p2.age=" << *p2.age << endl;
system("pause");
return 0;
}
4.5.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
4.5.6 函数调用运算符重载
- 函数调用用算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定的写法,非常灵活
仿函数和函数的区别:
- 仿函数: 对象(参数);来调用
- 函数: 函数名(参数);来调用
#include <iostream>
using namespace std;
//这是一个类
class MyPrint
{
public:
//函数调用运算符重载
void operator() (string str)
{
cout << str << endl;
}
};
class MyAdd
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
int main(){
//创建MyPrint对象
MyPrint print;
//仿函数
print("hello!");
//创建MyAdd对象
MyAdd add;
//仿函数
int a = add(2, 3);
cout << "a=" << a << endl;
//使用匿名函数对象,当前行使用完毕后就被释放
MyPrint()("WORLD!");
cout << MyAdd()(12, 20) << endl;
system("pause");
return 0;
}
4.6 继承
4.6.1 继承的语法
语法:class 子类(也叫派生类) : 继承方式 父类(也叫基类){}
4.6.2 继承方式
- 公共继承,public
- 保护继承,protected
- 私有继承,private
4.6.3 继承中的对象模型
问题:从父类继承过来的成员,有哪些是属于子类对象的?
答:父类中的所有非静态成员都会被子类继承,私有成员也会被子类继承,只是子类不能去访问。
(静态变量是和子类共享?子类共享父类的静态变量,子类修改此静态变量,父类的也会被修改)
通过开发人员命令提示符展示类的模型
1、开始菜单中,VS目录下找到开发人员命令提示窗口
2、切换到要展示模型的类所属的cpp文件所在的目录
3、输入指令 cl /d1 reportSingleClassLayout类名 “文件名.cpp”
注意:cl(英文l) /d1(数字1)
4.6.4 继承中的构造和析构顺序
子类继承父类之后创建子类对象时也会调用父类的构造函数,那么继承中的构造和析构顺序是怎样的呢?
**答:**父类构造函数先执行,析构函数后执行。
4.6.5 继承中同名成员的处理方式
当父类和子类中出现同名的成员,如何通过子类对象访问子类或者父类中的同名数据呢?
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域(只要是同名,就要加作用域,函数重载的也要加)
#include <iostream>
using namespace std;
class Base
{
public:
int a;
Base()
{
a = 100;
}
};
class Son :public Base
{
public:
int a;
Son()
{
a = 200;
}
};
int main()
{
Son s;
cout << "Son类中a=" << s.a << endl;
cout << "Son类中继承的a=" << s.Base::a << endl;
system("pause");
return 0;
}
4.6.6 继承中同名静态成员的处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
与非静态成员处理方式一致:
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域(只要是同名,就要加作用域,函数重载的也要加)
4.6.7 多继承语法
C++中允许一个类继承多个类
**语法:**class 子类: 继承方式 父类1,继承方式 父类2…
多继承可能会引发发父类中有同名成员出现,需要加作用域进行区分
C++实际开发中不建议使用多继承
4.6.8 菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被成为菱形继承或者钻石继承。
菱形继承的问题:
- 菱形继承子类都从派生类中继承了基类的数据,当该子类使用基类数据时,会出现二义性。
- 菱形继承子类从两个派生类中均可继承基类的数据,而基类的数据其只需要一份即可。
使用派生类虚继承基类的方式可以解决菱形继承带来的问题
虚继承语法:class A : virtual public B
虚继承的方式使菱形子类继承的是一个指针,通过记录偏移量使数据指向同一位置。
4.7 多态
4.7.1 多态的基本概念
多态使C++面向对象三大特性之一,指的是使用父类型的指针或者引用指向子类型的对象
多态分为2类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定–编译阶段确定函数地址
- 动态多态的函数地址晚绑定–运行阶段确定函数地址
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物叫" << endl;
}
};
class Cat : public Animal
{
public :
void speak()
{
cout << "喵喵喵" << endl;
}
};
void speak(Animal& animal)
{
animal.speak();
}
int main(){
Cat c;
//Animal类的speak函数前不加visual时,输出动物叫,因为地址在编译时已经绑定,静态多态
//Animal类的speak函数前加visual时,输出猫叫,动态多态
speak(c);
system("pause");
return 0;
}
4.7.2 多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展及维护
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是无意义的,主要是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类被称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include <iostream>
using namespace std;
//含有纯虚函数的类被称为抽象类
class Base
{
public:
//纯虚函数
virtual void print() = 0;
};
class Son : public Base
{
//重写父类函数
void print()
{
cout << "Son的print函数执行" << endl;
}
};
void print(Base& b)
{
b.print();
}
int main(){
//多态:父类型的指针指向子类型的对象
Base* b1 = new Son;
b1->print();
delete b1;
//多态:父类型的引用指向子类型的对象
Son s;
print(s);
system("pause");
return 0;
}
4.5.7 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
只有必须调用子类析构才能完成释放时采用此种方式,子类析构中没有必须执行的额外代码则不需要。
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
含有纯虚析构的类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类型(){}=0;
**注意:**对于纯虚析构,可以在类中写纯虚析构,类外写析构的实现,因为此类在某些情况下也会调用其析构函数。比如Son类继承Base类,使用多态Base调用Son,为了父类指针可以调用子类析构函数,将Animal类中的析构函数定义为纯虚构,但是Base类本身也需要调用析构函数,也需要进行实现。
//含有纯虚函数的类被称为抽象类
class Base
{
public:
//纯虚函数
virtual void print() = 0;
//纯虚析构
virtual ~Base() = 0;
};
//Base类析构函数的实现
Base::~Base()
{
cout << "Base的析构执行" << endl;
}
5 文件操作
程序运行时产生的数据都属于临时数据,程序一旦结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件
文件类型分为2中:
- 文本文件,文件以文本的ASCII码形式存储在计算机中
- 二进制文件,文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的3大类:
- ofstream 写操作 outputfile
- ifstream 读操作 inputfile
- fstream 读写操作 file
5.1 文本文件
5.1.1 写文件
步骤如下:
- 包含头文件, #include
- 创建流对象,ofstream ofs;
- 打开文件,ofs.open(“文件路径”,打开方式)
- 写数据,ofs <<“写入的数据”;
- 关闭文件,ofs.close();
//写文件
void write()
{
ofstream ofs;
ofs.open("d:\\mytxt.txt", ios::out | ios::trunc);
ofs << "窗前明月光\n疑是地上霜\n举头望明月\n低头思故乡";
ofs.close();
}
打开方式 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,先删除,再创建 |
ios::binary | 以二进制方式 |
可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
类似地,您如果想要打开一个文件用于读写,可以使用下面的语法:
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
5.1.2 读文件
步骤如下:
- 包含头文件, #include
- 创建流对象,ifstream ifs;
- 打开文件并判断文件是否打开成功,ifs.open(“文件路径”,打开方式),返回bool类型数据
- 读数据,4种读取方式;
- 关闭文件,ifs.close();
void read()
{
ifstream ifs;
ifs.open("d:\\mytxt.txt", ios::in);
char data[100];
/*
第一种方式
while(ifs >> data)
{
cout << data;
}
第二种方式,按行读,读不出换行
while (ifs.getline(data, sizeof(data)))
{
cout << data << endl;
}
第三种方式,需要包含string头文件,也是按行读
string s;
//getline参数为输入流对象和字符串
while(getline(ifs,s))
{
cout << s << endl;
}
*/
ifs.close();
}
5.2 二进制文件
文件打开方式要指定为 ios::binary
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型: ostream& write(const char* buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len时读写的字节数
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char* buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len时读写的字节数
//二进制写入文件
void binaryWrite()
{
ofstream ofs;
ofs.open("d:\\binarytxt.txt", ios::out | ios::binary);
Person p={11,"zhangsan"};
//这里要将Person的地址强转为字符指针
ofs.write((const char*)&p, sizeof(p));
ofs.close();
}
//二进制读文件
void binaruRead()
{
ifstream ifs("d:\\binarytxt.txt", ios::in | ios::binary);
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << p.age << endl;
cout << p.name << endl;
ifs.close();
}
C++进阶编程
- 本阶段主要针对C++泛型编程和STL技术做详细讲解,探讨C++更深层的使用
1 模板
1.1 模板的概念
模板就是建立通用的模具,大大提高复用性
1.2 函数模板
- C++的另一种编程思想称为泛型编程,主要利用的技术就是模板
- C++提供两种模板机制:函数模板和类模板
1.2.1 函数模板语法
函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体定制,用一个虚拟的类型来代表。
语法:
template<typename T>
函数声明或定义//这里指的是在函数模板下写函数的声明或者定义
解释:
template–声明创建模板
typename–表明其后面的符号是一种数据类型,可以用class代替
T --通用的数据类型,名称可以替换,通常为大写字母
1.2.2 函数模板的使用
- 自动类型推导
- 显示指定类型
#include <iostream>
using namespace std;
template<typename T >//声明以下是函数模板
void Mswap(T &a,T &b)
{
T temp = a;
a = b;
b = temp;
}
int main(){
int a = 10;
int b = 20;
//函数模板的使用
//1、自动类型推导
Mswap(a, b);
cout << "a=" << a << ";b=" << b << endl;
//2、显示指定类型
double c = 1.1;
double d = 2.2;
Mswap<double>(c,d);
cout << "c=" << c << ";d=" << d << endl;
system("pause");
return 0;
}
注意事项:
- 自动类型推导,必须推导出一致的数据类型T才可以使用
- 模板必须要确定出T的数据类型才可以使用
1.2.3 函数模板案例
数组排序,从大到小,算法用选择算法,测试char数组和int数组
#include <iostream>
using namespace std;
template<class T>//换位函数模板
void mswap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
template<class T>//排序函数模板
void msort(T arr[],int length)
{
//选择排序算法
//从第一个开始,每个元素和后边的元素相比较,后边的大则交换位置
for (int i=0;i<length;i++)
{
for (int j=i+1;j<length;j++)
{
if (arr[i]<arr[j])
{
mswap(arr[i], arr[j]);
}
}
}
}
template<class T>//输出数组函数模板
void printArray(T arr[], int length)
{
for (int i=0;i<length;i++)
{
cout << arr[i] <<" ";
}
}
int main(){
//字符数组测试
char arr1[] = "fgdhsle";
int charLength = sizeof(arr1) / sizeof(char);
msort(arr1, charLength);
printArray(arr1, charLength);//slhgfed
//整型数组测试
int arr2[] = { 2,4,7,9,3,34 };
int intLength = sizeof(arr2) / sizeof(int);
msort(arr2, intLength);
printArray(arr2, intLength);//34 9 7 4 3 2
system("pause");
return 0;
}
1.2.4 普通函数和函数模板的区别
区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
总结:建议使用显示指定类型的方式调用函数模板,自己确定通用类型T
1.2.5 普通函数和函数模板的调用规则
规则如下:
-
如果函数模板和普通函数都可以实现,优先调用普通函数(这里指系统调用的优先级)
-
可以通过空模板参数列表来强制调用函数模板
myPrint<>(a,b)
-
函数模板也可以发生重载
-
如果函数模板可以产生更好的匹配,优先调用函数模板
1.2.6 模板的局限性
当我们在函数中传入自定义数据类型时,无法进行比较操作。数组无法进行赋值操作。
解决方式:使用具体化的模板实现自定义类型的通用化
#include <iostream>
using namespace std;
class Person
{
public:
int age;
string name;
Person(int age, string name)
{
this->age = age;
this->name = name;
}
};
template<typename T>
bool byCompare(T a,T b)
{
if (a == b)
{
return true;
}
return false;
}
//单独为Person类写一个函数模板
template<> bool byCompare(Person p1, Person p2)
{
if (p1.age==p2.age && p1.name==p2.name)
{
return true;
}
return false;
}
int main(){
int a = 30;
int b = 40;
cout << byCompare(a, b)<< endl;//0
Person p1(12,"tom");
Person p2(12,"tom");
cout << byCompare(p1, p2) << endl;
system("pause");
return 0;
}
1.3 类模板
1.3.1 类模板语法
类模板的作用:建立一个通用类,其中成员的数据类型可以具体指定,用虚拟的类型来代表
语法:
template<typename T>
类
解释:
template – 声明创建模板
typename – 表明其后的符号是一种数据类型,可以用class代替
T – 通用数据类型,名称可以替换,通常为大写字母
1.3.2 类模板和函数模板的区别
- 类模板没有自动类型推导
- 类模板在模板参数列表中可以有默认参数
#include <iostream>
using namespace std;
//模板中可定义多种类型
//类模板可以设置默认参数类型
template<typename NameType,typename AgeType=int>
class Person
{
public:
NameType name;
AgeType age;
Person(NameType name, AgeType age)
{
this->name = name;
this->age = age;
}
};
int main(){
Person<string,int> p("张三",33);
cout << p.name << " " << p.age << endl;
//设置默认参数类型后,不指定参数类型的即默认为设置的默认参数类型
Person<string>p2("李四", 34);
cout << p2.name << " " << p2.age << endl;
system("pause");
return 0;
}
1.3.3 类模板中成员函数的创建时机
类模板中成员函数和普通类中成员函数的创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
1.3.4 类模板对象作函数参数
- 类模板实例化出的对象,向函数传参的方式
一共有3种
- 指定传入类型–直接显示对象的数据类型
- 参数模板化–将对象中的参数变为模板进行传递
- 整个类模板化–将这个对象类型模板化进行传递