本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。
C++程序在执行时,将内存大方向划分为4个区域
内存分区的意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程性
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
存放CPU执行的机器指令
代码是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码是只读的,使其只读的原因是防止程序意外的修改了它的指令
全局区:
全局变量、静态变量、全局常量、字符串常量存放在全局区
局部变量、局部常量不在全局区
该区域的数据在程序结束后由操作系统释放
static 修饰的叫静态,const 修饰的叫常量
栈区:
由编译器自动分配释放,存放函数的参数值(形参)和局部变量
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
x1int* func()2{3 int a = 10;4 return &a; //返回局部变量的地址,存放在栈区,栈区的数据在函数执行完自动释放5}6
7int main()8{9 int *p = func(); //指针接收返回的地址10 cout << *p << endl; //解引用,理论上是拿不到10的11 cout << *p << endl; //就算上面能拿到10,也是编译器一次短暂的保留,保留是不会一直持续下去的12 system("pause");13 return 0;14}堆区:
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
xxxxxxxxxx171int* func()2{3 //利用new关键字,可以将数据开辟到堆区4 //这里的指针本质上也是局部变量,它是放在栈区的,函数调用结束后就释放掉5 int *p = new int(10);6 return p;7}8
9int main()10{11 int *p = func(); 12 13 cout << *p << endl;14 cout << *p << endl;15 system("pause");16 return 0;17}C++中利用new操作符在堆区开辟数据
利用new创建的数据,会返回该数据对应的类型的指针
基本语法如下:
xxxxxxxxxx111//利用new创建整型数据2int *p = new int(10); //这里小括号里的10是数据的初始值3delete p; //释放4
5//利用new开辟一个整型数组6int *p = new int[10]; //这里中括号里的10是开辟数组的长度,开辟的数组没有初始值,需进行赋值操作7for(i=0;i<10;i++)8{9 p[i] = i;10}11delete[] p; //释放数组的时候要加一个中括号[]作用:给变量起别名
语法:数据类型 &别名 = 原名;
示例如下:
xxxxxxxxxx101int main()2{3 int a = 10;4 int &b = a; //创建引用5 6 b = 100;7 cout << "a = " << a << endl;8 cout << "b = " << b << endl; //此时对b操作就是对a操作,相当于两个捆绑在一起,值保持一致 9 10}以前学过两种交换函数的写法,分别是值传递和地址传递,但是值传递仅仅是形参的交换,它无法影响实参,故使用地址传递,现在用引用的方法也可以实现和地址传递同样的效果。示例如下:
xxxxxxxxxx401//值传递2void Swap01(int a,int b)3{4 int temp = a;5 a = b;6 b = temp;7}8
9//地址传递10void Swap02(int *a,int *b)11{12 int temp = *a;13 *a = *b;14 *b = temp;15}16//引用传递17void Swap03(int &a,int &b) //别名也可以和原名一样18{19 int temp = a;20 a = b;21 b = temp;22}23
24int main()25{26 int a = 10;27 int b = 20;28 29 //值传递调用30 Swap01(a,b);31 32 //地址传递调用33 Swap02(&a,&b);34 35 //引用传递调用36 Swap03(a,b);37 38 system("pause");39 return 0;40}1.不要返回局部变量的引用。
2.函数调用可作为左值。
示例如下:
xxxxxxxxxx131int& test01()2{3 int a = 10; //局部变量存放在四区中的栈区,在函数调用完之后会被释放4 return a;5}6
7int main()8{9 int &ref = test01();10 cout << "ref = " << ref << endl; //第一次结果正确,是因为编译器做了一次保留11 cout << "ref = " << ref << endl; //第二次结果错误,因为a的内存已经被释放12 13}xxxxxxxxxx171int& test02()2{3 static int a = 10; //静态变量存放在全局区,全局区上的数据在程序结束后系统释放4 return a;5}6
7int main()8{9 int &ref2 = test02();10 11 cout << "ref2 = " << ref2 << endl;12 cout << "ref2 = " << ref2 << endl; //静态变量不会释放13 14 test02() = 1000;15 cout << "ref2 = " << ref2 << endl;16 cout << "ref2 = " << ref2 << endl; //如果函数的返回值是引用,那么这个函数调用可作为左值17}本质:引用的本质在C++内部实现是一个指针常量
xxxxxxxxxx101int main()2{3 int a = 10;4 5 //自动转换为int * const ref = &a;指针常量是指针指向不可改变,也说明了引用为什么不可更改 6 int &ref = a;7 //内部发现ref是引用,自动帮我们转换为:*ref = 20;8 ref = 20;9 10}结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了
作用:常量引用主要用于修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参
示例如下:
xxxxxxxxxx231//引用使用场景,通常用来修饰形参2void showvalue(const int& v)3{4 //v = 20; 会报错!表达式左边必须为可修改的左值。这样做可以防止误操作,在一些只读性的函数调用中,防止形参影响实参5 cout << v << endl;6}7
8int mian()9{10 //int& ref = 10;引用本身需要一个合法的内存空间,表达式右边必须为一个变量,比如a,因此这行错误11 //加入const就可以了,编译器优化代码,int temp = 10; const int & ref = temp;12 const int & ref = 10;13 14 //ref = 100; //加入const后不可以修改变量15 cout << ref << endl;16 17 //函数中利用常量引用防止误操作修改实参18 int a = 10;19 showvalue(a);20 21 system("pause");22 return 0;23}在C++中,函数的形参列表的形参是可以有默认值的。
语法:返回值类型 函数名 (参数 = 默认值) {}
如果我们自己传入数据,就用我们自己的数据,否则就用默认值
注意事项:
1.如果某个位置参数有默认值,那么从这个位置往后,从左到右,必须都要有默认值
2.如果函数声明有默认值,函数实现的时候就不能有默认参数,声明和实现只能有一个有默认参数
xxxxxxxxxx121//1.如果某个位置参数有默认值,那么从这个位置往后,从左到右,必须都要有默认值2int func(int a ,int b = 10, int c = 10)3{4 return a + b + c;5}6
7//2.如果函数声明有默认值,函数实现的时候就不能有默认参数,声明和实现只能有一个有默认参数8int func2(int a = 10 ;int b = 10);9int func2(int a ,int b)10{11 return a + b;12}C++中函数的形参列表中可以有占位参数,用来作占位,调用函数时必须填补该位置
语法:返回值类型 函数名 (数据类型) {}
在现阶段中占位参数存在的意义不大,但是在后面的学习中会用到该技术
示例如下:
xxxxxxxxxx131//函数占位参数,占位参数也可以有默认参数2void func(int a ,int = 10) //对!这里没有写错,就是int = 10,当然这里也可以只写一个数据类型int3{4 cout << "This is a func!" << endl;5}6
7int main()8{9 func(10 ,10); //占位参数必须填补,当然有默认参数的时候可以不用填补\10 11 system("pause");12 return 0;13}作用:函数名可以相同,提高复用性
函数重载满足条件:
注意:函数的返回值不可以作为函数重载的条件
示例如下:
xxxxxxxxxx401//函数重载需要函数在同一个作用域下,这里都是全局作用域2void func()3{4 cout << "func()的调用!" << endl;5}6void func(int a)7{8 cout << "func(int a)的调用!" << endl;9}10void func(double a)11{12 cout << "func(double a)的调用!" << endl;13}14void func(int a ,double b)15{16 cout << "func(int a ,double b)的调用!" << endl;17}18void func(double a ,int b)19{20 cout << "func(double a ,int b)的调用!" << endl;21}22
23//函数返回值不可以作为函数重载的条件24//int func(double a ,int b)25//{26// cout << "func(double a ,int b)的调用!" << endl;27//}28
29
30int main()31{32 func();33 func(10);34 func(3.14);35 func(10,3.14);36 func(3.14,10);37 38 system("pause");39 return 0;40}示例如下:
xxxxxxxxxx231//函数重载注意事项2//1.引用作为重载条件3
4void func(int &a)5{6 cout << "func(int &a)的调用!" << endl;7}8
9void func(const int &a)10{11 cout << "func(const int &a)的调用!" << endl;12}13
14int main()15{16 int a = 10;17 18 func(a); //调用的是上面那个19 func(10); //调用的是下面那个20 21 system("pause");22 return 0;23}xxxxxxxxxx201//2.函数重载碰到函数默认参数2
3void func2(int a ,int b = 10)4{5 cout << "func2(int a ,int b = 10)的调用!" << endl;6}7
8void func2(int a)9{10 cout << "func2(int a)的调用!" << endl;11}12
13int main()14{15 func2(10); //产生歧义,不知道该调用哪个啦!16 func2(10,20); //不会产生歧义,调用上面那个17 18 system("pause");19 return 0;20}C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物皆为对象,对象有其属性和行为
封装是C++面向对象三大特性之一,封装的意义:
语法为:class 类名{访问权限:属性/行为}
示例:写一个圆类,并求出圆的周长
xxxxxxxxxx291//圆周率2const double PI = 3.14;3
4class Circle5{6public: //访问权限为公共的权限7 //属性8 int m_r; //半径9 10 //行为11 //获取圆的周长12 double calculateZC()13 {14 return 2 * PI * m_r;15 }16};17
18int main()19{20 //通过圆类,创建圆的对象21 //c1就是一个具体的圆22 Circle c1;23 c1.m_r = 10; //给圆对象的半径进行赋值操作24 25 cout << "圆的周长为:" << c1.calculateZC() << endl;26 27 system("pause");28 return 0;29}示例:创建一个学生类
xxxxxxxxxx361//使用字符串时需要2
3class Student{4public:5 //属性6 string m_name;7 int m_id;8 9 //行为10 void setname(string name)11 {12 m_name = name;13 }14 15 void setid(int id)16 {17 m_id = id;18 }19 20 void showStudent()21 {22 cout << "姓名:" << m_name << "学号:" << m_id << endl;23 }24};25
26int main()27{28 Student s1;29 s1.setname("张三");30 s1.setid(9527);31 32 s1.showStudent();33 34 system("pause");35 return 0;36}访问权限有三种:
示例如下:
xxxxxxxxxx291class Person{2public:3 string m_name; //姓名 公共权限4 5protected:6 string m_car; //汽车 保护权限7 8private:9 int m_password; //银行卡密码 私有权限10 11public:12 void func()13 {14 m_name = "张三";15 m_car = "拖拉机";16 m_password = 8940886;17 }18};19
20int main()21{22 Person p1;23 p1.m_name = "李四"; //这是可以成功的,因为m_name的权限为public24 //p1.m_car = "奔驰";保护权限类外访问不到25 //p1.m_password = "1234567";私有权限,类外访问不到26 27 system("pause");28 return 0;29}不论是属性还是行为,都要遵守权限规则,定义在protected和private下的行为(函数/方法)同样无法在类外访问到。
在C++中struct和class的唯一区别就在于默认的访问权限不同:
struct的默认权限为公共,class的默认权限为私有
优点:
示例如下:
xxxxxxxxxx4312
3class Person{4private:5 string m_name;6 int age;7public:8 //通过这个接口,可以在类外间接设置私有属性9 void setname(string name)10 {11 m_name = name;12 }13 //通过这个接口,可以在类外访问到属性14 string getname()15 {16 return m_name;17 }18 19 //对于写权限,我们可以检测数据的有效性20 void setage(int age)21 {22 if(age < 0 || age >100)23 {24 m_age = 0; //给默认值25 cout << "你这个输入的年龄不太合理!" << endl;26 }27 m_age = age;28 }29};30
31int main()32{33 Person p;34 //p.m_name = "张三";是会报错的,因为m_name为私有访问权限35 p.setname("张三");36 //cout << "姓名:" << p.m_name << endl;同样也会报错37 cout << "姓名" << p.getname() << endl;38 39 p.setage(18); //设置年龄40 41 system("pause");42 return 0;43}对象的初始化和清理是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用的后果是未知的。
同样的使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题。
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,并且编译器提供的构造函数和析构函数是空实现。
构造函数语法:类名 () {}
void析构函数语法:~类名 () {}
void~xxxxxxxxxx261class Person{2public:3 Person()4 {5 cout << "Person的构造函数调用" << endl;6 }7 ~Person()8 {9 cout << "Person的析构函数调用" << endl;10 }11};12
13void func()14{15 Person p;16}17
18int main()19{20 func(); //构造和析构都会调用,因为子函数里的对象在栈区,函数调用结束后会自动释放,释放的时候调用析构21 22 Person p; //这是主函数里面创建一个对象,就只会调用构造函数,而析构函数不会被调用23 24 system("pause");25 return 0;26}两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式: 括号法、显示法、隐式转换法
示例如下:
xxxxxxxxxx431class Person{2public:3 Person()4 {5 cout << "无参构造函数!" << endl;6 }7 Person(int a)8 {9 age = a;10 cout << "有参构造函数!" << endl;11 }12 Person(const Person &p)13 {14 age = p.age;15 cout << "拷贝构造函数!" << endl;16 }17 18 ~Person()19 {20 cout << "析构函数调用!" << endl;21 }22public:23 int age;24};25
26void test01()27{28 //括号法29 Person p1; //无参构造函数30 Person p2(10); //有参构造函数31 Person p3(p2); //拷贝构造函数32 //注意的是无参构造不要加括号,比如:Person p1();这样编译器会认为这行代码是一个函数声明,而不是一个对象的创建33 34 //显示法35 Person p1; //无参构造函数36 Person p2 = Person (10); //有参构造函数37 Person p3 = Person (p2); //拷贝构造函数38 39 //隐式转换法40 Person p4 = 10; //相当于写了 Person p4 = person (10); 编译器会隐式的转换41 Person p5 = p4; //拷贝构造函数42 43}括号法注意事项:
Person p1(); 不要这样写,加了括号,编译器会认为这行代码是一个函数声明,而不是一个对象的创建。
显示法注意事项:
1.单独拿出等号右侧,Person (10),这是一个匿名对象,它在执行完当前行后,系统会立即回收掉匿名对象。
2.不要利用拷贝构造函数,初始化匿名对象。Person (p3); 编译器会认为它等价于 Person p3,这样的话,由于上面也定义过p3,就会出现重定义的报错!
C++中拷贝构造函数调用时机通常有三种情况
示例如下:
xxxxxxxxxx541class Person{2public:3 Person()4 {5 cout << "无参构造函数!" << endl;6 }7 8 Person(int a)9 {10 age = a;11 cout << "有参构造函数!" << endl;12 }13 14 Person(const Person &p)15 {16 age = p.age;17 cout << "拷贝构造函数!" << endl;18 }19 20 ~Person()21 {22 cout << "析构函数调用!" << endl;23 }24public:25 int age;26};27
28void test01()29{30 Person p1(10);31 Person p2(p1); //使用一个已经创建完毕的对象来初始化一个新的对象32 cout << "p2的年龄为:" << p2.age << endl; //输出应该为1033}34
35void test02(Person p) //值传递的方式给函数参数传值,会发生拷贝构造36{37 cout << "p的年龄为:" << p.age << endl;38}39
40//以值方式返回局部对象41Person doWork()42{43 Person p1;44 cout << (int*)&p1 << endl;45 return p1;46}47void test03()48{49 Person p = doWork();50 cout << (int*)&p << endl;51}52
53//调用test03()的运行结果为:先无参构造p1,然后打印对象p1的地址,return的时候先发生拷贝构造,再析构对象p154//打印对象p的地址,析构对象p默认情况下,C++编译器至少给一个类添加3个函数
构造函数调用规则如下:
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作
示例如下:
xxxxxxxxxx521class Person{2public:3 Person()4 {5 cout << "无参构造函数!" << endl;6 }7 8 Person(int age,int height)9 {10 m_age = age;11 m_height = new int(height);12 13 cout << "有参构造函数!" << endl;14 }15 16 Person(const Person &p)17 {18 m_age = p.m_age;19 //m_height = p.m_height; //编译器自带的浅拷贝操作20 m_height = new int(*p.m_height); //自己写的拷贝构造函数,使用的是在堆区重新申请空间进行深拷贝操作21 22 //如果使用编译器自带的浅拷贝,会在析构的时候发生重复释放内存空间的问题23 cout << "拷贝构造函数!" << endl;24 }25 26 ~Person()27 {28 if(m_height != NULL)29 {30 delete m_height;31 m_height = NULL;32 }33 cout << "析构函数调用!" << endl;34 }35public:36 int m_age;37 int *m_height;38};39
40void test()41{42 Person p1(18,175);43 Person p2(p1);44}45
46int main()47{48 test();49 50 system("pause");51 return 0;52}总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来内存空间重复释放的问题。
作用:C++提供了初始化列表语法,用来初始化属性
语法为:构造函数():属性1(值1),属性2(值2),属性3(值3)...{}
示例如下:
xxxxxxxxxx161class Person{2public:3 Person (int a,int b,int c) :m_A(a),m_B(b),m_C(c)4 {5 6 }7 8 int m_A;9 int m_B;10 int m_C;11};12
13void test()14{15 Person p(10,20,30);16}C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
当B类中有对象A作为成员,A和B的构造和析构的顺序谁先谁后?
xxxxxxxxxx61class A{};2
3class B{4public:5 A a;6};构造顺序为:A先,B后
析构顺序为:B先,A后
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为:
静态成员变量
静态成员函数
示例1:静态成员变量
xxxxxxxxxx391class Person{2public:3 static int m_A; //静态成员变量4 5private:6 static int m_B; //静态成员变量也是有访问权限的7 8};9
10//类内声明,类外初始化11int Person::m_A = 10;12int Person::m_B = 10;13
14void test01()15{16 //静态成员变量的两种访问方式17 //1.通过对象18 Person p1;19 p1.m_A = 100;20 cout << "p1.m_A = " << p1.m_A << endl; //输出10021 22 Person p2;23 p2.m_A = 200;24 cout << "p1.m_A = " << p1.m_A << endl;25 cout << "p2.m_A = " << p2.m_A << endl; //共享同一份数据,因此两行输出都是20026 27 //2.通过类名28 cout << "m_A = " << Person::m_A << endl;29 30 //cout << "m_B = " << Person::m_B << endl; //私有权限访问不到31}32
33int main()34{35 text01();36 37 system("pause");38 return 0;39}示例2:静态成员函数
xxxxxxxxxx401class Person{2public:3 static void func()4 {5 cout << "static void func()的调用" << endl;6 m_A = 100;7 //m_B = 200;错误,静态成员函数不可以访问非静态成员变量,静态成员函数只能访问静态成员变量8 }9 10 static int m_A;11 int m_B;12private:13 static void func2()14 {15 cout << "static void func2()的调用" << endl;16 }17};18int Person::m_A = 10;19
20void test01()21{22 //静态成员函数的两种访问方式23 //1.通过对象24 Person p1;25 p1.func();26 27 //2.通过类名28 Person::func();29 30 //Person::func2(); //私有权限访问不到31 32}33
34int main()35{36 text01();37 38 system("pause");39 return 0;40}在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上,静态成员变量、非静态成员函数、静态成员函数都不属于类的对象上
空对象占用的内存空间为1个字节,C++编译器会给每个空对象分配1个字节空间,是为了区分空对象占内存的位置,每个空对象应该有一个独一无二的内存地址。
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分是哪个对象调用自己呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可。
this指针的用途
return *this示例如下:
xxxxxxxxxx251class Person{2public:3 Person(int age)4 {5 //当形参和成员变量同名时,可以用this指针来区分6 this->age = age; //写age = age;会由于无法区分形参和成员变量而出错!7 }8 9 Person& PersonAddPerson(Person p) //如果这里&去掉的话,返回的是值,返回的不是p2的本体,而是拷贝出的新的数据10 { //如果这里&去掉,后续输出应该是2011 this->age += p.age;12 return *this;13 }14 int age;15};16
17void test01()18{19 Person p1(10);20 cout << "p1.age" << p1.age << endl;21 22 Person p2(10);23 P2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); //链式编程思想,应该输出4024 cout << "p2.age" << p2.age << endl;25}C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
示例如下:
xxxxxxxxxx251class Person{2public:3 void ShowClassName()4 {5 cout << "this is Person class" << endl;6 }7 void ShowPersonAge()8 {9 if(this == NULL)10 {11 return ;12 }13 //报错的原因是传入的this指针是空指针14 cout << "age = " << this->m_age << endl;15 }16 17 int m_age;18};19
20void test01()21{22 Person *p = NULL;23 p->ShowClassName();24 p->ShowPersonAge(); //如果没有if语句,程序会因为这行代码崩溃25}常函数:
const,我们称这个函数为常函数mutable,在常函数中依然可以修改常对象:
const称该对象为常对象示例如下:
xxxxxxxxxx311class Person{2public:3 void showPerson() const4 {5 //m_a = 100;成员函数后面加上const后,就不能修改属性值了6 m_b = 100; //因为成员属性声明时加关键字`mutable`,在常函数中依然可以修改7 }8 9 void func(){}10 11 int m_a;12 mutable int m_b; //特殊变量,即使在常函数中也是可以修改值的13};14
15void test01()16{17 Person p;18 p.showPerson();19}20
21void test02()22{23 const Person p; //在对象前加const,变为常对象24 //p.m_a = 100; //报错,常对象的属性也是不允许修改的25 p.m_b = 100; //特殊变量,在常对象下也可以修改26 27 //常对象只能调用常函数28 p.showPerson(); //允许29 //p.func();会报错,常对象只能调用常函数,因为在普通成员函数中是允许修改属性值的,这与常对象的准则不符30 31}this指针的本质是一个指针常量,它的指向是不可以修改的,在成员函数后加const后,限定指针指向和指向的值都不可以修改了。
生活中你的家有客厅(public),有你的卧室(private)。客厅所有的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性也想让类外特殊的一些函数或者类访问,就需要用到友元技术,友元的目的就是让一个函数或者类访问另一个类中的私有成员
友元的关键字为friend
友元的三中实现
示例如下:
xxxxxxxxxx371class Building2{3 //告诉编译器goodgay全局函数是Building类的好朋友,可以访问类中的私有内容4 friend void goodgay(Building *building);5 6public:7 Building()8 {9 this->m_sittingroom = "客厅";10 this->m_bedroom = "卧室";11 }12public:13 string m_sittingroom; //客厅14private:15 string m_bedroom; //卧室16};17
18void goodgay(Building *building)19{20 cout << "好基友正在访问:" << building->m_sittingroom << endl; //正常访问21 cout << "好基友正在访问:" << building->m_bedroom << endl;22 //没有类里面的友元声明,是不允许在类外访问私有属性的,下面这行代码会报错!23}24
25void test01()26{27 Building b;28 goodgay(&b);29}30
31int main()32{33 test01();34 35 system("pause");36 return 0;37}示例如下:
xxxxxxxxxx491class goodgay2{3public:4 goodgay()5 {6 building = new Building;7 }8 void visit(); //成员函数写在类外9 10private:11 Building *building;12};13
14void goodgay::visit()15{16 cout << "好基友正在访问:" << building->m_sittingroom << endl;17 cout << "好基友正在访问:" << building->m_bedroom << endl;18}19
20class Building21{22 //告诉编译器 goodgay类是Building类的好朋友,可以访问到Building类中的私有内容23 friend class goodgay;24 25public:26 Building()27 {28 this->m_sittingroom = "客厅";29 this->m_bedroom = "卧室";30 }31public:32 string m_sittingroom; //客厅33private:34 string m_bedroom; //卧室35};36
37void test01()38{39 goodgay gg;40 gg.visit();41}42
43int main()44{45 test01();46 47 system("pause");48 return 0;49}也可以将成员函数写在类外面,比如上面示例中 goodgay 类中的 visit() 成员函数。
与全局函数、类做友元类似。核心代码如下:
xxxxxxxxxx161class Building2{3 //告诉编译器 goodgay类下的visit成员函数作为本类的好朋友,可以访问本类的私有属性4 friend void goodgay::visit();5 6public:7 Building()8 {9 this->m_sittingroom = "客厅";10 this->m_bedroom = "卧室";11 }12public:13 string m_sittingroom; //客厅14private:15 string m_bedroom; //卧室16};运算符重载的概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
作用:实现两个自定义数据类型的相加运算
示例1:成员函数实现 + 号运算符重载
xxxxxxxxxx321class Person 2{3public:4 Person(int a ,int b)5 {6 this->m_A = a;7 this->m_B = b;8 }9 10 //成员函数实现 + 号运算符重载11 Person operator+(Person &p)12 {13 Person temp;14 temp.m_A = this->m_A + p.m_A;15 temp.m_B = this->m_B + p.m_B;16 return temp;17 }18public:19 int m_A;20 int m_B;21};22
23void test01()24{25 Person p1(10,20);26 Person p2(30,40);27 28 Person p3 = p1 + p2; //本质是Person p3 = p1.operator+(p2);29 cout << "p3.m_A = " << p3.m_A << endl;30 cout << "p3.m_B = " << p3.m_B << endl;31}32
示例2:全局函数实现 + 号运算符重载
xxxxxxxxxx331class Person 2{3public:4 Person(int a ,int b)5 {6 this->m_A = a;7 this->m_B = b;8 }9 10public:11 int m_A;12 int m_B;13};14
15//全局函数实现 + 号运算符重载16Person operator+(Person &p1,Person &p2)17{18 Person temp;19 temp.m_A = p1.m_A + p2.m_A;20 temp.m_B = p1.m_B + p2.m_B;21 return temp;22}23
24void test02()25{26 Person p1(10,20);27 Person p2(30,40);28 29 Person p3 = p1 + p2; //本质是Person p3 = operator+(p1,p2);30 cout << "p3.m_A = " << p3.m_A << endl;31 cout << "p3.m_B = " << p3.m_B << endl;32}33
示例3:运算符重载也可以发生函数重载,以下以实现Person + int为例
xxxxxxxxxx451class Person 2{3public:4 Person(int a ,int b)5 {6 this->m_A = a;7 this->m_B = b;8 }9 10public:11 int m_A;12 int m_B;13};14
15//全局函数实现 + 号运算符重载16Person operator+(Person &p1,Person &p2)17{18 Person temp;19 temp.m_A = p1.m_A + p2.m_A;20 temp.m_B = p1.m_B + p2.m_B;21 return temp;22}23
24//运算符重载也可以发生函数重载,以实现Person + int25Person operator+(Person &p1,int num)26{27 Person temp;28 temp.m_A = p1.m_A + num;29 temp.m_B = p1.m_B + num;30 return temp;31}32
33void test03()34{35 Person p1(10,20);36 Person p2(30,40);37 38 Person p3 = p1 + p2;39 cout << "p3.m_A = " << p3.m_A << endl;40 cout << "p3.m_B = " << p3.m_B << endl; //输出结果为40和6041 42 Person p4 = p1 + 100;43 cout << "p4.m_A = " << p4.m_A << endl;44 cout << "p4.m_B = " << p4.m_B << endl; //输出结果为110和12045}注意:1.对于内置的数据类型的表达式的运算符是不能改变的。2.不要滥用运算符重载。
作用:剖析cout的本质,可以输出自定义的数据类型。
示例如下:
xxxxxxxxxx321class Person()2{3public:4 Person(int a,int b)5 {6 m_a = a;7 m_b = b;8 }9 10 //成员函数实现不了左移运算符重载,p.operator<<(cout) 简化后为 p << cout 不是我们想要的结果11 //void operator<<(ostream cout)12 //{13 // 14 //}15 //成员函数实现的话 p 始终在左边16 17public:18 int m_a;19 int m_b;20};21
22ostream& operator<<(ostream &cout,Person &p) //链式调用的编程思想23{24 cout << "m_a = " << p.m_a << "m_b = " << p.m_b;25 return cout;26}27
28void test01()29{30 Person p(10,20);31 cout << p << "hello" << endl;32}总结:有些时候成员变量属性是private,这时重载左移运算符配合友元可以实现输出自定义数据类型。
作用:通过重载递增运算符,实现自己的整型数据。
示例如下:
xxxxxxxxxx511class MyInteger2{3 4public:5 MyInteger() //构造函数赋初值为06 {7 m_num = 0;8 }9 10 //重载左移运算符11 ostream& operator<<(ostream &cout,MyInteger myint)12 {13 cout << myint.m_num;14 return cout;15 }16 17 //前置++18 MyInteger& operator++()19 {20 m_num++; //先递增21 return *this; //再返回,注意要返回的是引用22 }23 24 //后置++25 //MyInteger operator++(int) 这里int代表占位参数,可用于区分前置++和后置++26 //而且只能用int做占位参数,double、float什么的都不行27 MyInteger operator++(int)28 {29 MyInteger temp = *this; //先记录下值30 m_num++; //再递增31 return temp; //返回之前记录的值,这样就实现了先用老值,再递增的操作32 }33 34public:35 int m_num;36};37
38void test01()39{40 MyInteger myint;41 42 cout << ++MyInteger << endl;43 cout << MyInteger << endl;44}45void test02()46{47 MyInteger myint;48 49 cout << MyInteger++ << endl;50 cout << MyInteger << endl;51}注意:重载前置++返回为引用,重载后置++返回为值,而且重载后置++需要用int做占位参数
C++编译器至少给一个类添加4个函数
operator=,对属性进行值拷贝如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝的问题,即堆区内存重复释放。
示例如下:
xxxxxxxxxx431class Person2{3public:4 Person(int age)5 {6 //将年龄开辟到堆区7 m_age = new int(age);8 }9 10 //重载赋值运算符11 Person& operator=(Person &p)12 {13 if(m_age != NULL)14 {15 delete m_age;16 m_age = NULL;17 }18 19 //编译器提供的代码是浅拷贝20 //m_age = p.m_age;21 22 //我们提供深拷贝,解决浅拷贝出现的堆区内存重复释放的问题23 m_age = new int(*p.m_age);24 25 //返回自身26 return *this;27 }28 29public:30 int* m_age;31}32
33void test01()34{35 Person p1(18);36 Person p2(20);37 person p3(25);38 39 p3 = p2 = p1; //链式编程思想40 cout << "p1的年龄为:" << *p1.m_age << endl;41 cout << "p2的年龄为:" << *p2.m_age << endl;42 cout << "p3的年龄为:" << *p3.m_age << endl;43}作用:重载关系运算符,可以让两个自定义类型的对象进行对比操作
示例如下:
xxxxxxxxxx381class Person2{3public:4 Person(string name,int age)5 {6 m_name = name;7 m_age = age;8 }9 10 //重载关系运算符11 bool operator==(Person &p)12 {13 if(m_name == p.m_name && m_age == p.m_age)14 {15 return true;16 }17 return false;18 }19 20public:21 string m_name;22 int m_age;23}24
25void test01()26{27 Person p1("tom",18);28 Person p2("tom",18);29 30 if(p1 == p2)31 {32 cout << "p1和p2是相等的" << endl;33 }34 else35 {36 cout << "p1和p2是不相等的" << endl;37 }38}示例如下:
xxxxxxxxxx311class MyPrint2{3public:4 //重载的()操作符也称仿函数5 void operator()(string str)6 {7 cout << str << endl;8 }9};10
11class MyAdd12{13public:14 int operator()(int num1,int num2)15 {16 return num1 + num2;17 }18};19
20void test()21{22 MyPrint myprint;23 myprint("hello world!");24 25 MyAdd myadd;26 cout << myadd(10,20) << endl;27 28 //也可以使用匿名对象29 cout << MyAdd()(10,20) << endl;30 31}继承是面向对象三大特性之一
继承的好处:减少重复的代码
语法:class 子类 : 继承方式 父类 {};
子类也称派生类,父类也称基类。
子类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过来的表现其共性,而自己新增的成员表现其个性。
继承的语法:class 子类 : 继承方式 父类 {};
继承一共有3种方式:
特点:这3种继承方式子类都不能访问到父类中的私有成员属性,公共继承,子类继承的父类的公共属性仍为公共属性、保护属性仍为保护属性;保护继承,子类继承的父类的公共属性变为保护属性、保护属性仍为保护属性;私有继承,子类继承的父类的公共属性、保护属性全部变为私有属性。
公共属性在类内和类外都可以访问到;保护属性在类内可以访问、在类外不可以访问;私有属性在类内可以访问、在类外不可以访问。
问题:从父类继承过来的成员,哪些属于子类对象中?
结论:父类中的私有成员也是被子类继承下去了,只是由于编译器给隐藏后访问不到。
利用工具(开发人员命令提示符)查看的步骤:
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
结论:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反,先析构子类,再析构父类。
问题:当子类和父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
示例如下:
xxxxxxxxxx511class Base2{3public:4 Base()5 {6 m_a = 100;7 }8 9 void func()10 {11 cout << "Base中的func()调用" << endl;12 }13 14 void func(int a)15 {16 cout << "Base中的func(int a)调用" << endl;17 }18public:19 int m_a;20};21
22class Son :public23{24public:25 Son()26 {27 m_a = 200;28 }29 30 //当子类和父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数(包括重载的函数)31 //如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域32 void func()33 {34 cout << "Son中的func()调用" << endl;35 }36public:37 int m_a;38};39
40void test01()41{42 Son s;43 44 cout << "Son 下的m_a = " << s.m_a << endl;45 cout << "Base下的m_a = " << s.Base::m_a << endl;46 47 s.func(); //如果子类没有定义这个成员函数,会直接调用父类中的func()省去了写作用域48 s.Base::func(); //一旦子类中定义了func()这个成员函数,如果想访问父类中的func()函数,就必须加父类的作用域49 s.Base::func(10);50}51
总结:
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问方式(通过对象和通过类名)
要理解两个双冒号的意义不同,例如:
xxxxxxxxxx261//同名成员属性2void test01()3{4 //1.通过对象访问5 Son s;6 cout << "Son 下的m_a = " << s.m_a << endl;7 cout << "Base下的m_a = " << s.Base::m_a << endl;8 9 //2.通过类名访问10 cout << "Son 下的m_a = " << Son::m_a << endl;11 cout << "Base下的m_a = " << Son::Base::m_a << endl;12}13
14//同名成员函数15void test02()16{17 //1.通过对象访问18 Son s;19 s.func();20 s.Base::func();21 22 //2.通过类名访问23 Son::func(); //出现同名,子类会隐藏掉父类中所有的同名成员函数,需要加作用域来进行访问24 Son::Base::func();25 Son::Base::func(10);26}C++允许一个类继承多个类
语法:class 子类 :继承方式 父类1,继承方式 父类2 ...{};
多继承可能会引发父类中有同名成员出现,需要加作用域区分,所以C++在实际开发中不建议用多继承。
菱形继承的概念:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承就叫做菱形继承也叫钻石继承。
示例如下:
xxxxxxxxxx241class Animal2{3public:4 int m_age;5};6
7//继承前加virtual关键字后,变为虚继承8//此时公共的父类Animal称为虚基类9class Sheep :virtual public Animal{};10class Tuo :virtual public Animal{};11class SheepTuo :public Sheep,public Tuo{};12
13void test()14{15 SheepTuo st;16 st.Sheep::m_age = 18;17 st.Tuo::m_age = 28;18 19 cout << "st.Sheep::m_age = " << st.Sheep::m_age << endl;20 cout << "st.Tuo::m_age = " << st.Tuo::m_age << endl;21 cout << "st.m_age = " << st.m_age << endl;22 //虚继承后,只有一份数据,不会出现二义性,这里三个输出都为2823 24}注意:虚继承后继承的是一个vbptr指针(v--virtual、b--base、prt--pointer),该指针指向虚继承表,表里面记录了到虚基类那份独一无二的成员属性的偏移量,这样就确保了孙子类里面的数据只有一份,直接访问也不会出现二义性。
总结:
多态是C++面向对象三大特性之一
多态分为两类
静态多态和动态多态的区别:
示例如下:
xxxxxxxxxx491class Animal2{3public:4 //speak函数就是虚函数5 //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数的调用了,实现了晚绑定6 virtual void speak()7 {8 cout << "动物在说话" << endl;9 }10};11
12class Cat : public Animal13{14public:15 void speak()16 {17 cout << "小猫在说话" << endl;18 }19};20
21class Dog : public Animal22{23 public:24 virtual void speak() //子类里面这个virtual可写可不写25 {26 cout << "小狗在说话" << endl;27 }28};29
30//我们希望传入什么对象,就调用什么对象的函数31void dospeak(Animal &animal)32{33 animal.speak();34}35//多态满足的条件:36//1.有继承关系37//2.子类重写父类中的虚函数,父类的同名函数前加virtual关键字38//多态的使用39//父类指针或引用指向子类对象40
41void test()42{43 Cat cat;44 dospeak(cat); //这里Animal &animal = cat 即父类指针或引用指向子类对象45 46 Dog dog;47 dospeak(dog); //这里Animal &animal = dog 即父类指针或引用指向子类对象48 49}总结:
多态满足的条件
多态的使用条件
重写:函数返回值类型、函数名、参数列表完全一致称为重写,注意区分重载。
多态的优点:
C++开发提倡利用多态设计程序架构,因为多态优点很多。
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类中重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
虚析构与纯虚析构的区别:
虚析构语法:virtual ~类名(){父类析构代码}
纯虚析构语法:类内写声明virtual ~类名() = 0;类外写实现 类名::~类名(){父类析构代码}
总结:
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化。
C++中对文件操作需要包含头文件<fstream>
文件类型分为两种:
操作文件的三大类:
写文件的步骤如下:
#include <fstream>ofstream ofs;ofs.open("文件路径",打开方式);ofs << "写入的数据" ;ofs.close();常见的文件打开方式如下:
| 打开方式 | 解释 |
|---|---|
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用|操作符。例如:用二进制的方式写文件 ios::binary | ios::out
总结:
#include <fstream>读文件与写文件的步骤相似,但是读取方式相对比较多
读文件步骤如下:
#include <fstream>ifstream ifs;ifs.open("文件路径",打开方式);ifs.close();示例如下:
xxxxxxxxxx44123
4void test()5{6 ifstream ifs;7 ifs.open("text.txt",ios::in);8 9 if(!ifs.is_open())10 {11 cout << "文件打开失败!" << endl;12 return;13 }14 15 //第一种读取方式16 char buf[1024] = {0};17 while(ifs >> buf)18 {19 cout << buf <<endl;20 }21 22 //第二种读取方式23 char buf[1024] = {0};24 while(ifs.getline(buf,sizeof(buf)))25 {26 cout << buf << endl;27 }28 29 //第三种读取方式30 string buf;31 while(getline(ifs,buf))32 {33 cout << buf << endl;34 }35 36 //第四种读取方式,一般用的少,一个字符一个字符去读效率低37 char c;38 while((c = ifs.get()) != EOF)39 {40 cout << c;41 }42 43 ifs.close();44}总结:
以二进制的方式对文件进行读写操作,打开方式要指定为 iOS::binary
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
示例如下:
xxxxxxxxxx30123
4class Person5{6public:7 char m_name[64];8 int m_age;9};10
11//二进制文件 写文件12void test01()13{14 //1.包含头文件15 16 //2.创建输出流对象17 ofstream ofs("person.txt",ios::out | ios::binary);18 19 //3.打开文件20 //ofs.open("person.txt",ios::out | ios::binary);21 22 Person p = {"张三",18};23 24 //4.写文件25 ofs.write((const char*)&p,sizeof(p));26 27 //5.关闭文件28 ofs.close();29 30}总结:文件输出流对象可以通过write函数,以二进制的方式写数据
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
示例如下:
xxxxxxxxxx241void test02()2{3 //1.包含头文件4 5 //2.创建流对象6 ifstream ifs;7 8 //3.打开文件,判断文件是否打开成功9 ifs.open("person.txt",ios::in | ios::binary);10 if(!ifs.is_open())11 {12 cout << "文件打开失败!" << endl;13 return;14 }15 16 Person p;17 18 //4.读文件19 ifs.read((char*)&p,sizeof(p));20 cout << "姓名:" << p.m_name << "年龄:" << p.m_age << endl;21 22 //5.关闭文件23 ifs.close();24}总结:文件输入流对象可以通过read函数,以二进制的方式读数据