第一部分 基础语法入门
一、基础
1、变量与常量
1、变量
变量存在的意义:方便管理内存空间
2、常量
用于记录程序中不可更改的数据
#define 常量名 常量值
const 数据类型 常量名=常量值 ;
2、数据类型
1、整型
short 2字节
int 4字节
long Win4字节,32位linux 4字节,64位linux 8字节
long long 8字节
2、实型(浮点型)
float 4字节 7位有效数字
double 8字节 15-16位有效数组
默认情况下,输出一位小数,会显示6位有效数字
科学计数法
float f1=3e2; //3*10^2
float f2=3e-2; //3*0.1^2
3、字符型
char 1字节
字符型并不是把字符本身放在内存中存储,而是将对应的ASCII编码存储
4、字符串
char 变量名[]="abc"
string 变量名="abc" //include <string>
5、bool
1字节
6、数组
int a[5];
int b[5]={1,2,3,4,5};
int c[]={1,2,3};
3、sizeof关键字
统计数据类型所占内存大小
4、案例
cpp#include <stdio.h> #include <stdlib.h> #include <string.h> void qOne() { int a = 4; a += (a++);//9 5+=4 执行a++ 返回4,a递增为5 a += (++a);//10 5+=5 //(a++)+= a; /* a++是一个右值,即不可修改的临时值,+=需要一个左值(即可以被赋值或修改的对象) */ (++a) += (a++);//10 /* ++a a变为5,返回5,++a作为左值 a++ 当前a为5,返回5,a自增为6 5+=5 */ printf("%d\n", a); } void Foo(char str[100]) { // 将str被视为char*传递,所以打印的是指针大小 printf("%d\n", sizeof(str)); // 8 32位操作系统中,指针大小4字节,64位8字节 } void qTwo() { char str[] = "Hello word"; char *p = str; int n = 10; printf("%d\n", sizeof(str)); // 11 printf("%d\n", sizeof(p)); // 64位8 printf("%d\n", sizeof(n)); // 4 // Foo(str); void *p1 = malloc(100); // sizeof(p1) 返回的是指针本身的大小,而不是 malloc 分配的内存的大小。指针的大小依赖于编译器和操作系统的位数。 printf("%d\n", sizeof(p1)); // 8 } struct Test { int Num; bool b; char sz[10]; }; void qThree() { // 使用了c++的特性new,不能使用gcc编译 // Test* p=new Test[10]; struct Test *p = (struct Test *)malloc(10 * sizeof(struct Test)); Test *p1 = &p[0]; Test *p2 = &p[8]; int n = p2 - p1; printf("%d", n); // 8 } void qThreePlus() { // 使用 malloc 分配内存,分配空间足够存放10个 Test 结构体 void *p = new Test[10]; /* void *p1 = (char *)p; // 指向第一个 Test 结构体 void *p2 = (char *)p + 8 * sizeof(struct Test); // 指向第九个 Test 结构体 printf("sizeof(struct Test):%d\n", sizeof(struct Test)); // 因为 p1 和 p2 是 void*,我们需要将它们转换为适当的类型才能进行运算 int n = (char *)p2 - (char *)p1;//128 */ /* Test *p1 = (Test *)((char *)p + 0 * sizeof(struct Test)); // 指向第一个 Test 结构体 Test *p2 = (Test *)((char *)p + 8 * sizeof(struct Test)); // 指向第九个 Test 结构体 int n = p2 - p1; */ Test *p1 = (Test *)p; // 指向第一个 Test 结构体 Test *p2 = (Test *)p + 8; // 指向第九个 Test 结构体 int n = p2 - p1; printf("%d\n", n); // 输出 8 } char *qFour() { char *a = new char[128]; char *p = a; strcpy(p, "aaaa"); return p; } char *qFive() { // 拷贝不了:局部变量,栈上分配内存,函数结束,内存空间会被回收 // char a[128]; // 使用malloc或静态数组或者new,生命周期将被延长 static char a[128]; // 为什么不能是&a,他是指向包含128字符的数组的指针,不是字符,类型是char (*)[128],不是char* char *p = a; strcpy(p, "aaaa"); return p; } char *strcpy(char *strDest, const char *strSrc) { char *oStrDest = strDest; while (*strSrc != '\0') { *strDest = *strSrc; strSrc++; strDest++; } *strDest = '\0'; return oStrDest; } char *strcpyPlus(char *strDest, const char *strSrc) { int srcLen = strlen(strSrc); char *oStrDest = strDest; if (strDest < strSrc || strDest - strSrc >= srcLen) { // 没有内存重叠问题 while (*strSrc != '\0') { *strDest = *strSrc; strSrc++; strDest++; } *strDest = '\0'; return oStrDest; } // 有内存重叠问题 strDest = strDest + srcLen; strSrc = strSrc + srcLen - 1; *strDest = '\0'; for (int i = 0; i < srcLen; i++) { strDest--; *strDest = *strSrc; strSrc--; } return oStrDest; } void *memcpy(void *dest, const void *src, int size) { char *cDest = (char *)dest; const char *cSrc = (const char *)src; for (int i = 0; i < size; i++) { cDest[i] = cSrc[i]; } return dest; } void *memcpyOtherWay(void *dest, const void *src, int size) { char *cDest = (char *)dest; const char *cSrc = (const char *)src; for (int i = 0; i < size; i++) { *cDest = *cSrc; cDest++; cSrc++; } return dest; }
二、函数
1、函数的分文件编写
作用:让代码结构更加清晰
1、创建.h头文件
2、创建.cpp源文件
3、在头文件写函数的声明
4、在源文件写函数的定义
cpp//值交换 //1、swap.h #include <iostream> using namespace std; void swap(int a,int b); //2、swap.cpp #include "swap.h" void swap(int a,int b){ int temp=a; a=b; b=temp; cout << "a=" << a <<endl; cout << "b=" << b <<endl; } //3、main.cpp #include <iostream> #inculde "swap.h" using namespace std; int main(){ swap(1,2);. return 0; }
2、函数默认参数
函数的形参列表是可以有默认值的
cpp#include <iostream> using namespace std; int add(int a,int b,int c); int add(int a,int b=20,int c=30){ return a+b+c; } int main(){ add(10);//60 add(10,30);//70 } /* 1、如果某个位置已经有了默认参数,那么从这个位置往后,从左往右都必须有默认值int add(int a,int b=20,int c=30) 2、如果函数声明有默认参数,函数的实现就不能有默认参数(声明和实现只能有一个有默认参数) */
3、函数占位参数
cpp#include<iostream> using namespace std; void func1(int a,int){ } //占位参数可以有默认参数 void func2(int a,int =20){ } int main(){ func1(10,20); }
4、函数重载
条件:
1、同一个作用域
2、函数名称相同
3、函数参数类型不同或者个数不同或者顺序不同
4、返回值不能作为重载条件
cpp#include<iostream> using namespace std; //作用让函数名相同,提高复用性 void func1(){ } void func1(int a){ } //引用作为重载条件 void func2(int &a){ } void func2(const int &a){ } //函数重载碰到默认参数 语法通过调用传入1参数会产生二义性 void func3(int a){ } void func3(int a,int b=10){ } int main(){ int a=10; func2(a);//第一个 因为a是可读可写的 func2(10);//第二个 有const合法 }
三、指针
1、指针的定义和使用
1、作用:可以通过指针间接访问内存
(1)内存编号是从0开始记录的,一般用16进制表示
(2)可以通过指针变量保存地址
启动一个程序,系统会给其分配内存空间,内存空间最小1字节,内存中每一个字节都有编号,这个编号称为内存的地址
2、定义与使用
cpp#include <iostream> using namespace std; int main(){ //定义指针 int a=10; int* p; p=&a; //指针就是地址 cout << "a的地址" << &a <<endl; cout << "指针p为" << p << endl; //0xABCD //使用指针 //可以通过解引用(*)的方式来找到指针指向的内存 *p=100; cout << "a=" << a <<endl;//100 return 0; }
2、 指针所占的内存空间
32位操作系统指针占用4字节,64位8个字节
3、空指针与野指针
1、空指针
指针变量指向内存中编号为0的空间
用途:用于初始化指针变量
注意:空指针指向的内存空间不可以访问
内存编号0~255为系统占用内存,不允许用户访问
cpp#include <iostream> using namespace std; int main(){ //空指针:用于给指针变量初始化,不初始化就不知道指向哪里 int *p=NULL; //语法通过,但是不可以访问,0~255之间内存编号不允许访问 *p=100; return 0; }
2、野指针
指针变量指向非法的内存空间,不是我们申请的空间
cpp#include <iostream> using namespace std; int main(){ //指针指向内存地址编号为0x1100的空间 int* p=(int*)0x1100; //访问野指针出错 cout<< *p <<endl; return 0; }
3、万能指针
不可以定义void类型的变量,因为编译器不知道给变量分配多大的空间,但是可以定义void *类型,因为指针都是4个字节
cppint main() { int a = 10; void *p = (void *)&a; // printf("%d\n", *p);//err p是void*,不知道取几个字节的大小 printf("%d\n", *(int *)p2); // 10 }
4、const修饰指针
const修饰指针:常量指针
int a=10;
const int* p=&a;(const修饰的是int* 所以值不能改)
指针的指向可以改,但该指针指向的值不可以改
const修饰常量:指针常量
int a=10;
int* const p=&a;(const修饰的是p 所以地址不能改)
const后面跟的p是变量,修饰后成为常量
指针的指向不能改,但指向的值可以改
const即修饰指针又修饰常量
const int* const p=&a;
5、指针和数组
int a[5];
数组名a代表数组,也代表第0个元素地址
在数值上:a==&a[0]==&a==地址
若&a[0]=0x0001,则&a[0]+1=0x0005(元素地址+1=跨过一个元素),等同于a+1
&a+1跨过整个数组==0x0021
cppvoid array() { int a[5]; /* 数组名a代表数组,也代表第0个元素地址 a==&a[0]==&a==地址 若&a[0]==01,则&a[0]+1=05(元素地址+1跨过一个元素),等同于a+1 &a+1跨过整个数组=21 */ printf("%d\n", a); printf("%d\n", &a); printf("%d\n", &a[0]); printf("%d\n", &a[0] + 1); printf("%d\n", &a[1]); printf("%d\n", a + 1); printf("%d\n", &a + 1); printf("%d\n", &a[4] + 1); }
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
a[0][0]第0行第0个元素
&a[0][0]第0行第0个元素地址 +1跨过一个元素
a[0]第0行一维数组名 +1跨过一个元素
&a[0]第0行地址 +1跨过一行
a二维数组名,首行地址&a[0] +1跨过一行
&a二维数组地址 +1跨过整个数组
//元素个数int num=sizeof(a)/sizeof(a[0][0]);
//行数 总大小/一行的大小
int row=sizeof(a)/sizeof(a[0]);
//列数 行大小/一个元素大小
int column=sizeof(a[0])/sizeof(a[0][0]);
6、指针和函数
cppvoid swap(int *p1,int *p2){ int temp=*p1; *p1=*p2; *p2=temp; } int main(){ int a=10; int b=20; swap(&a,&b); }
cppvoid bubbleSort(int * arr,int len){//int *arr可以写成int arr[] for(int i=0;i<len-1;i++){ for(int j=0;j<len-i-1;j++){ if(arr[j]>arr[j+1]){ int temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp; } } } }
cpp#include <stdio.h> int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int main() { int num = 10; int *pNum = # // 基本指针类型 printf("%d\n", *pNum); // 10 printf("%p\n", &pNum); printf("%p\n", pNum); // 保存的num的地址,和&num一样 printf("%p", &num); // 函数指针 int (*operation)(int, int); operation=add; printf("a+b=%d\n",operation(5,3)); return 0; } /** * 指针==地址==编号 * 指针变量:存放指针(地址)的变量 * 指针: * 基本指针类型 * int* char* float* double* void*可以指向任何类型数据,使用前需类型转换 * 指向指针的指针 * int** 指向int*的指针,即指针的指针,用于动态数组等场景 * 函数指针:指向这个函数 * 返回值类型 (*函数指针名)(参数类型) * 在使用时,对一个表达式取*,就会对表达式减一级*,如果对表达式取&,就会加一级* * 32位操作系统中,指针大小4字节,64位8字节 * * * */
四、结构体
1、定义
struct 类型名称{
成员列表
};
cppstruct Student { int age; string name; }s3; int main(){ //1、struct Student s1其中struct可以省略 struct Student s1; s1.name="张三"; s1.age=18; //2、struct Student s2={...} struct Student s2={18,"张三"} //3、定义时顺便赋值s3 s3.age=20; s3.name="王五"; }
2、结构体数组
struct 数组名[个数]={{},{},{}...}
cppstruct Student{ string name; int age; }; int main(){ struct Student stuArr[2]={ {"张三",18}, {"王五",20} }; stuArr[1].name="张武"; }
3、结构体指针
->
cppstruct Student{ string name; int age; }; int main(){ Student s={"张三",18}; Student* p=&s; p->name="王五"; }
4、结构体嵌套结构体
cppstruct student{ string name; int age; }; struct teacher{ string name; int age; struct student stu; }; int main(){ teacher t; t.stu.name="小小"; }
5、结构体做函数参数
cppstruct Student { int age; string name; }; void printStu1(struct Student s){ } void printStu2(struct Student *p){ p->name="李四"; p->age=32; } int main(){ struct Student s; s.name="张三"; s.age=18; //值传递 printStu1(s); //址传递 printStu2(&s) }
如果不想修改主函数中的数据,用值传递,反之用地址传递
6、结构体中const使用
作用:用const防止误操作
cppstruct student{ string name; int age; }; void printStu(const student * s){ } int main(){ student s={"张三",20}; printStu(&s); }
值传递会复制副本,所以速度慢
7、案例
cppstruct Student{ string name; int age; }; struct Teacher{ string name; struct Student arr[5]; }; void allocateSpace(struct Teacher arr[],int len){ string nameSeed="ABCDE"; for(int i=0;i<len;i++){ arr[i].name="Teacher_"; arr[i].name+=nameSeed[i]; for(int j=0;j<5;j++){ arr[i][j].name="Student_"; arr[i][j].name+=nameSeed[j]; arr[i][j].age=18; } } } int main(){ Teacher tArr[3]; int len=sizeof(tArr)/sizeof(tArr[0]); allocateSpace(tArr,len); }
第二部分 核心编程
一、内存模型
C++程序执行时,将内存大致分为4个区域
代码区:存放函数体的二进制代码,由操作系统进行管理
全局区:存放全局变量和静态变量以及常量
栈区:由编译器进行自动分配与释放,存放函数的参数值,局部变量
堆区:由程序员分配和释放,若不释放,程序结束由操作系统回收
1、程序运行前
在程序编译后,生成exe可执行程序,未执行程序前分为两个区域
代码区:
存放CPU执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量(static)
包含了常量区:字符串常量和其他常量(const修饰的全局变量)也存放在此
该区域数据在程序结束后由操作系统释放
注意:局部变量和局部常量不在全局区
2、 程序运行后
分为栈区和堆区
栈区
由编译器自动分配释放,存放函数参数值,局部变量
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区
new
delete
释放数组delete[] arr;
二、引用
1、引用的基本使用
作用:给变量起别名
语法:数据类型 &别名=原名
2、引用的注意事项
引用必须初始化
初始化后不可改变
cppint main(){ int a=10; int &b=a; }
3、引用做函数参数
cppvoid swap(int &a,int &b){ //形参中的a是main中a的别名 int temp=a; b=a; a=temp; } int main(){ int a=20; int b=20; swap(a,b); }
4、引用做函数返回值
不要返回局部变量的引用
函数的调用可以作为左值
cppint& func(){ static int a=10; return a; } int main(){ int &ref=func();//相当于int* const ref=&a ref=20;//内部发现ref为引用,自动转为*ref=20 test()=1000; }
5、引用的本质
引用的本质是一个指针常量(指针指向不可变)
6、常量引用
作用:常量引用主要用来修饰形参,防止形参改变实参
cppvoid showValue(const int& v){ //v+=10; } int main(){ int a=10; showVlue(a); //int& ref=10;引用本身需要一个合法的内存空间,因此错误 const int& ref=10;//加上const之后,编译器将代码进行修改 int temp=10;const int& ref=temp; //ref=20;加入const之后成为只读,不可修改 }
三、类和对象
1、封装
cpp#include<iostream> #include<string> using namespace std; class Student{ public: //成员属性/成员变量 string m_Name; int m_Id; //成员函数/成员方法 void show(){ } void set(string name,int id){ this->m_Name=name; this->m_Id=id; } }; int main(){ Student s1; s1.m_Name="张三"; s1.m_Id=1; s1.show(); }
权限
public 公共权限 成员类内可以访问,类外可以访问
protected 保护权限 成员类内可以访问,类外不可以访问 继承子可以访问父保护内容
private 私有权限 成员类内可以访问,类外不能访问
struct和class的区别为:默认访问权限不同struct:public
class:private
2、对象的初始化和清理
2.1 构造与析构
构造函数
有参构造/无参构造
普通构造/拷贝构造
析构函数:不可以有参数,不能重载
cpp#include<iostream> using namespace std; class Person{ public: Person(){ //没有返回值 函数名和类名相同 可以有参数,可以重载 创建对象时,会自动调用一次 默认提供 } Person(string name,int age){ this->name=name; thia->age=age; } //拷贝构造 Person(const Person &p){ this->age=p.age; this->name=p.name; } ~Person(){ //没有返回值 函数名和类名相同 不可以有参数 对象销毁前,会自动调用一次 默认提供 } private: string name; int age; }; int main(){ //构造函数调用方式1 Person p1;//栈上的数据,main执行完毕会释放 Person p2("li",10); Person p3(p2); //不会创建对象,编译器会认为是函数的声明 Person p4(); //构造函数调用方式2 Person p5=Person("zhang",30); //匿名对象 当程序执行结束后,系统立刻回收(这行代码执行后回收) Person("zhang",30); //不能利用拷贝构造函数初始化匿名对象,编译器会任务Person(p3) === Person p3; 对象声明重定义 构造函数调用方式3 隐式调用-拷贝构造 Person p6=p3; }
2.2 拷贝构造调用时机
拷贝构造函数调用时机
使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部对象
cpp#include<iostream> using namespace std; class Pereson{ public: Person (){ } Person(int age){ this->age=age; } Person(const Person &p){ this->age=p.age; } ~Person(){ } private: int age; }; void doWork1(Person p){ //值传递的本质会拷贝出一个临时副本出来 } Person doWork2(){ Person p1; //会根据p1拷贝出一个新的对象返回 return p1; } //拷贝构造函数调用时机 void demo1(){ //1、使用一个已经创建完毕的对象来初始化一个新对象 Person p1(10); Person p2(p1); //2、值传递的方式给函数参数传值 Person p3; doWork1(p3); //3、值传递返回局部对象 Person p4=doWork2(); }
2.3 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
1、默认构造
2、默认析构
3、默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则
如果用户定义有参构造,c++不再提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
2.4 深拷贝与浅拷贝
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
总结:如果属性有在堆区开辟内存的,一定要提供拷贝构造函数,防止浅拷贝带来的问题
cpp#include<iostream> using namespace std; class Person{ public: Person(){} Person(int age,int height){ this->age=age; //堆区创建的返回就是int* this->height=new int(height); } ~Person(){ if(height!=NULL){ delete height; height=NULL; } } private: int age; int *height; }; void demo1(){ Person p1(18,160); //报错 Person p2(p1); /* 原因:创建了p1,p2两个对象 如果利用编译器提供的拷贝构造函数,会做浅拷贝操作 堆区开辟的空间,浅拷贝将地址拷贝 执行析构时p2先被释放,同时释放堆区内存,p1在释放就会出错 浅拷贝的问题就是堆区内存重复释放,使用深拷贝解决 */ }
cpp#include<iostream> using namespace std; class Person{ public: Person(){} Person(int age,int height){ this->age=age; //堆区创建的返回就是int* this->height=new int(height); } Person(const Person &p){ //深拷贝 this->age=age; this->height=new int(*p.height); } ~Person(){ if(height!=NULL){ delete height; height=NULL; } } private: int age; int *height; }; void demo1(){ Person p1(18,160); Person p2(p1); }
2.5 初始化列表
初始化列表
cpp#include <iostream> using namespace std; class Person{ public: //初始化列表初始化 Person():age(10),height(20){} Person(int a,int h):age(a),height(h){} private: int age; int height; };
2.6 类对象作为类成员
类对象作为类成员
cpp#include<iostream> using namespace std; class A{ }; class B{ public: A a; }; void demo1(){ B b; } //先有A再有B
当其他类对象作为本类成员,构造时候先构建类对象,在构造自身,先析构本身,在析构类对象
2.7 静态成员
静态成员
静态成员就是在成员变量和成员函数前加上关键字static
静态成员变量
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
cpp#include<iostream> using namespace std; class A{ public: static int m_A; static void func(){} }; int A::m_A=100; //静态成员变量,不属于某个对象,所有对象共享同一份数据 //因此,两种访问方式:通过对象访问/通过类名访问 void demo1(){ A a1; //a1.m_A=200; A a2; a2.m_A=200; //A::m_A=300; }
3、c++对象模型和this
3.1 成员变量和成员函数
成员变量和成员函数分开存储
cpp#include<iostream> using namespace std; /* 变量只能存储在 min(他的长度,pack参数)的整数倍地址上 char 地址为1的倍数 short 2的倍数 0 2 4 8 int 4的倍数 0 4 8 12 double 8的倍数 0 8 16 24 结构体整体对齐跟他的 min(最长的字段,pack)整数倍对齐 数组按照数组类型来对齐 结构体嵌套结构体按照被嵌套的最大元素长度对齐 */ struct AA{ long long a; //8 char b; // int c; //和12对齐 char d[2]; //偏移量目前16+2+(6)=8*3 } struct BB{ long long a;//8 char b;//1+7 struct AA c;//按8*2对齐 char d[2];//偏移量16+24+2+(6)=8*6 } /* #pragma pack(show) 默认16 //16一般超过结构体中最大大小,所以没影响 如果 #pragma pack(2) 那么int地址 0 2 4 6 struct CC{ long long a; //8 char b; //2 int c; //和10对齐 char d[2]; //偏移量目前14+2=2*8 } */
cpp#include<iostream> class A{}; class Person{ public: int a;//非静态成员变量 属于类的对象 4字节 static int b; //静态成员变量 不属于类对象 void func1(){}//非静态成员函数 不属于类的对象 static void func2(){}//静态成员函数 不属于类的对象 }; int Person::b; void demo1(){ //空对象占用内存空间sizeof(a) 为1字节 A a; //C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占用内存的位置 每个空对象也应该有一个独一无二的内存地址 } void demo2(){ Person p; //sizeof(p); 4字节 只有非静态成员变量属于类的对象 }
3.2 this指针
this指针
成员变量和成员函数分开存储
每个非静态成员函数只会诞生一份函数实例,也就是多个同类型的对象会公用一块代码
this指针指向被调用的成员函数所属的对象
this指针的用途
当形参和成员变量同名时,使用this区分
在类的非静态成员函数中返回对象本身,可使用return *this,this指针指向对象,解引用返回对象本身
cpp#include<iostream> using namespace std; class Person{ public: //如果返回值写Person 会调用拷贝构造函数返回新的对象 Person& PersonAddAge(Person &p){ this->age+=p.age; return *this; } int age; }; void demo1(){ Person p1; p1.age=10; Person p2; p2.age=10; p2.PersonAddAge(p1).PersonAddAge(p2);//30 }
空指针可以访问成员函数,但要注意this,空指针不能访问属性
3.3 常函数与常对象
const修饰成员函数
成员函数加const成为常函数
常函数不可以修改成员属性
成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象
声明对象前加const称为常对象
常对象只能调用常函数
cpp#include <iostream> using namespace std; class Person{ public: //this指针的本质 指针常量 指针的指向不可以改变 //加const 本质修饰的this指针,让指针指向的值不可以改变 void show() const{ //this->a=100; this->b=10; } int a; mutable int b;//在常函数中也想修改 }; void demo2(){ const Person p;//常对象,属性不可以修改,只能调用常函数,不能调用普通函数,因为普通函数可以修改属性 //p.a=100; p.b=100; }
4、友元
友元:让一个函数或者类,访问另一个类中私有成员
全局函数做友元
cpp#include<iostream> using namespace std; class Building{ //goodGuy可以访问私有成员了 friend void goodGuy(Building *building); public: Building(){ this->sittingRoom="客厅"; this->bedRoom="卧室;" } public: string sittingRoom; private; string bedRoom; }; //全局函数 void goodGuy(Building *building){ println(building->sittingRoom); println(building->bedRoom); } void demo1(){ Building building; goodGuy(&building); }
类做友元
cpp#include<iostream> using namespace std; //声明 class Building; class Building{ //GoodGuye类可以访问私有成员了 friend class GoodGuy; public: Building(); public: string sittingRoom; private; string bedRoom; }; //类外可以初始化成员函数 Building::Building(){ this->sittingRoom="客厅"; this->bedRoom="卧室;" } class GoodGuy{ public: GoodGuy(){ building=new Building; } void visit(); Building *building; }; void GoodGuy::visit(){ println(building->sittingRoom); println(building->bedRoom); } void demo1(){ GoodGuy goodGuy; goodGuy.visit(); }
成员函数做友元
cpp#include<iostream> using namespace std; class Building{ //visit方法可以访问私有成员了 friend void GoodGuy::visit(); public: Building(); public: string sittingRoom; private; string bedRoom; }; //类外可以初始化成员函数 Building::Building(){ this->sittingRoom="客厅"; this->bedRoom="卧室;" } class GoodGuy{ public: GoodGuy(); void visit(); Building *building; }; GoodGuy::GoodGuy(){ buildint=new Building; } void GoodGuy::visit(){ println(building->sittingRoom); println(building->bedRoom); } void demo1(){ GoodGuy goodGuy; goodGuy.visit(); }
5、运算符重载
运算符重载:对已有运算符重新进行定义,赋予另一种功能,以适应不同数据类型
加号运算符重载
cpp#include<iostream> using namespace std; //1、成员函数重载+ class Person{ public: int a; int b; Person operator+(const Person &p){ Person temp; temp.a=this->a+p.a; temp.b=this->b+p.b; return temp; } }; //2、全局函数重载+ Person operator+(const Person &p1,const Person &p2){ Person temp; temp.a=p1.a+p2.a; temp.b=p1.b+p2.b; return temp; } void demo1(){ Person p1; p1.a=10; p1.b=20; Perosn p2; p2.a=30; p2.b=40; Person p3=p1+p2; //本质调用 Person p3=p1.operator+(p2) //Person p3=operator+(p1,p2) //p3.a==40;p3.b==60 }
cpp#include <iostream> using namespace std; class Person{ friend ostream& opetator<<(ostream &cout,Perosn &p); private: int a ; int b; }; ostream& opetator<<(ostream &cout,Perosn &p){ cout<<"a="<<p.a<<" b="<<p.b; return cout; }
cppclass MyInteger { friend ostream& operator<<(ostream& out, MyInteger myint); public: MyInteger() { m_Num = 0; } //前置++ MyInteger& operator++() { //先++ m_Num++; //再返回 return *this; } //后置++ MyInteger operator++(int) { //先返回 MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++; m_Num++; return temp; } private: int m_Num; }; ostream& operator<<(ostream& out, MyInteger myint) { out << myint.m_Num; return out; } //前置++ 先++ 再返回 void test01() { MyInteger myInt; cout << ++myInt << endl; cout << myInt << endl; } //后置++ 先返回 再++ void test02() { MyInteger myInt; cout << myInt++ << endl; cout << myInt << endl; }
cppclass Person { public: Person(int age) { //将年龄数据开辟到堆区 m_Age = new int(age); } //重载赋值运算符 Person& operator=(Person &p) { if (m_Age != NULL) { delete m_Age; m_Age = NULL; } //编译器提供的代码是浅拷贝 //m_Age = p.m_Age; //提供深拷贝 解决浅拷贝的问题 m_Age = new int(*p.m_Age); //返回自身 return *this; } ~Person() { if (m_Age != NULL) { delete m_Age; m_Age = NULL; } } //年龄的指针 int *m_Age; };
cppclass Person { public: Person(string name, int age) { this->m_Name = name; this->m_Age = age; }; bool operator==(Person & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return true; } else { return false; } } bool operator!=(Person & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return false; } else { return true; } } string m_Name; int m_Age; }; void test01() { //int a = 0; //int b = 0; Person a("孙悟空", 18); Person b("孙悟空", 18); if (a == b) { cout << "a和b相等" << endl; } else { cout << "a和b不相等" << endl; } if (a != b) { cout << "a和b不相等" << endl; } else { cout << "a和b相等" << endl; } }
cppclass MyPrint { public: void operator()(string text) { cout << text << endl; } }; void test01() { //重载的()操作符 也称为仿函数 MyPrint myFunc; myFunc("hello world"); } class MyAdd { public: int operator()(int v1, int v2) { return v1 + v2; } }; void test02() { MyAdd add; int ret = add(10, 10); cout << "ret = " << ret << endl; //匿名对象调用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl; }
6、继承
6.1 基本语法
cppclass Base{}; class A:public Base{ }
class 子类(派生类): 继承方式 父类(基类)
6.2 继承方式
公共继承
保护基础
私有继承
缩小了权限
6.3 继承中的对象模型
父类中所有非静态成员属性都会被子类继承下去
6.4 继承中构造和析构顺序
父类构造--子类构造--子类析构--父类析构
6.5 继承同名成员处理方式
当子类与父类出现同名的成员的成员,如何通过子类对象,访问子类或父类同名数据
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
6.6 继承同名静态成员处理方式
静态成员和非静态成员出现同名
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
cppclass Base { public: static void func() { cout << "Base - static void func()" << endl; } static void func(int a) { cout << "Base - static void func(int a)" << endl; } static int m_A; }; int Base::m_A = 100; class Son : public Base { public: static void func() { cout << "Son - static void func()" << endl; } static int m_A; }; int Son::m_A = 200; //同名成员属性 void test01() { //通过对象访问 cout << "通过对象访问: " << endl; Son s; cout << "Son 下 m_A = " << s.m_A << endl; cout << "Base 下 m_A = " << s.Base::m_A << endl; //通过类名访问 cout << "通过类名访问: " << endl; cout << "Son 下 m_A = " << Son::m_A << endl; cout << "Base 下 m_A = " << Son::Base::m_A << endl; } //同名成员函数 void test02() { //通过对象访问 cout << "通过对象访问: " << endl; Son s; s.func(); s.Base::func(); cout << "通过类名访问: " << endl; Son::func(); Son::Base::func(); //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问 Son::Base::func(100); }
6.7 多继承
class 子类:继承方式 父类1,继承方式 父类2
6.8 菱形继承
菱形继承:
两个派生类继承同一个基类
某个类同时继承两个派生类
cl /d1 reportSingleClassLayout类 file.cpp
cppclass 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; }
- 虚继承可以解决菱形继承问题
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义利用
7、多态
7.1 基本概念
cpp#include<iostream> using namespace std; class Animal{ public: //如果想执行让猫说话,那函数地址就不能提前绑定需要在运行期间绑定--地址晚绑定 void speak(){ //virtual void speak(){ println("dongwu"); } }; class Cat :public Animal{ public: void speak(){ println("miao"); } }; class Dog:public Animal{ public: void speak(){ println("wang"); } }; void doSpeak(Animal &animal){ animal.speak(); } void demo1(){ Cat cat; doSpeak(cat); //执行的时动物说话,因为地址早绑定,在编译阶段就确定了函数地址 }
动态多态满足条件
有继承关系
子类重写父类虚函数
父类的指针/引用 指向子类的对象
7.2 多态剖析
写了一个虚函数virtual void speak(),类的内部发生了改变,多了一个虚函数指针(vfptr),指向虚函数表(vftable),表中记录着 虚函数的入口地址,当子类继承了父类,同样会继承父类结构,当子类重写父类虚函数,会将自身虚函数表中虚函数入口地址替换为子类虚函数地址。所以当父类指针或引用指向子类对象时,发生多态
7.3 纯虚函数和抽象类
当类中有了纯虚函数,这个类称为抽象类
无法实例化对象
子类必须重写抽象类中纯虚函数,否则也属于抽象类
cppclass Base { public: //纯虚函数 //类中只要有一个纯虚函数就称为抽象类 //抽象类无法实例化对象 //子类必须重写父类中的纯虚函数,否则也属于抽象类 virtual void func() = 0; }; class Son :public Base { public: virtual void func() { cout << "func调用" << endl; }; }; void test01() { Base * base = NULL; //base = new Base; // 错误,抽象类无法实例化对象 base = new Son; base->func(); delete base;//记得销毁 }
7.4 饮品
cpp#include <iostream> using namespace std; class AbstractDrink{ public: //煮水 virtual void Boil()=0; //冲泡 virtual void Brew()=0; //倒杯 virtual void PourInCup()=0; //加入佐料 virtual void PutSomething()=0; //制作饮品 void makeDrink(){ Boil(); Brew(); PourInCup(); PutSomething(); } }; Class Coffee :public AbstractDrink{ void Boil(){ } void Brew(){ } void PourInCup(){ } void PutSomething(){ } }; void doWork(AbstractDrink *drink){ drink->makeDrink(); delete drink; } void demo1(){ doWork(new Coffee); }
7.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类析构
解决:将父类中析构函数改为虚析构或纯虚析构
cpp#include<iostream> using namespace std; class Animal{ public: virtual void speak()=0; virtual ~Animal(){} }; class Cat :public Animal{ public: Cat(string name){ this->name=new string(name); } void speak(){ } string *name; ~Cat(){ if (this->name!=NULL){ delete this->name; this->name=NULL; } } }; void demo1(){ Animal *animal=new Cat("加菲"); animal->speak(); //父类指针在析构的时候,不会调用子类中析构,导致子类中如果有堆区数据,造成内存泄漏 delete animal; }
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
拥有纯虚析构函数的类也属于抽象类
7.6 电脑组装
cpp#include<iostream> using namespace std; class CPU{ public: virtual void calculate()=0; }; class VideoCard{ public: virtual void display()=0; }; class Memory{ public: virtual void storage()=0; }; class IntelCpu:public CPU{ void calculate(){} }; class IntelVideoCard:public VideoCard{ void display(){} }; class IntelMemory:public Memory{ void storage(){} }; class Computer{ public: Computer(CPU *cpu,VideoCard *vc,Memory *mem){ this->cpu=cpu; this->vc=vc; this->mem=mem; } void doWork(){ cpu->calculate(); vc->display(); mem->storage(); } ~Computer(){ if(this->cpu!=NULL){ delete this->CPU; this->cpu=NULL; } if(this->vc!=NULL){ delete this->vc; this->vc=NULL; } if(this->mem!=NULL){ delete this->mem; this->mem=NULL; } } private: CPU *cpu; VideoCard *vc; Memory *mem; }; void demo1(){ Computer * com=new Computer(new InterCPU,new InterVideoCard,new InterMemory); com->doWork(); delete com; }
8、文件操作
8.1 文本文件
头文件<fstream>
文本文件:文本以文本的ASCII码的形式存储
二进制文件:以二进制的形式存储
三大类:
ofstream:写操作
ifstream:读操作
fstream:读写
文本文件写文件1、包含头文件
#include<fstream>
2、创建流对象
ofstream ofs;
3、打开文件
ofs.open("路径",打开方式);
4、写数据
ofs<<"写入的数据";
5、关闭文件
ofs.close();
文件打开方式
|-------------|---------------|
| 打开方式 | 解释 |
| ios::in | 为读文件而打开文件 |
| ios:out | 写文件 |
| ios:ate | 初始位置:文件尾 |
| ios:app | 追加方式写文件 |
| ios:trunc | 如果文件存在先删除,在创建 |
| ios::binary | 二进制方式 |注意:文件打开方式可以配合使用,利用|操作符
cpp#include <fstream> void test01() { ofstream ofs; ofs.open("test.txt", ios::out); ofs << "姓名:张三" << endl; ofs << "性别:男" << endl; ofs << "年龄:18" << endl; ofs.close(); }
文本文件读文件
cpp#include<iostream> using namespace std; #include<fstream> void demo1(){ ifstream ifs; ifs.open("test.txt",ios::in); if(!ifs.is_open()){ return; } //1、字符数组 char buf[1024]={0}; while(ifs>>buf){ cout<<buf<<endl; } //2、字符数组 char buf[1024]={0}; while(ifs.getline(buf,sizeof(buf))){ cout<<buf<<endl; } //3、string string buf; while(getline(ifs,buf)){ cout<<buf<<endl; } //4、char c char c; while((c=ifs.get())!=EOF){/EOF end of file cout<<c; } ifs.close(); }
8.2 二进制文件
写文件
cpp#include<iostream> using namespace std; #include <fstream> class Person{ public: char m_Name[64]; int m_Age; }; void demo1(){ ofstream ofs; ofs.open("person.txt",ios::out|ios::binary); Person p={"张三",18}; ofs.write((const char*)&p,sizeof(Person)); ofs.close(); }
读文件
cpp#include<iostream> using namespace std; #include <fstream> class Person{ public: char m_Name[64]; int m_Age; }; void demo1(){ ifstream ifs; ofs.open("person.txt",ios::in|ios::binary); if(!ifs.is_open()){ return; } Person p; ifs.read((char*)&p,sizeof(Person)); //p.m_Name p.a_Age ofs.close(); }