本阶段主要针对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}
堆区:
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
xxxxxxxxxx
171int* 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创建的数据,会返回该数据对应的类型的指针
基本语法如下:
xxxxxxxxxx
111//利用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; //释放数组的时候要加一个中括号[]
作用:给变量起别名
语法:数据类型 &别名 = 原名;
示例如下:
xxxxxxxxxx
101int 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}
以前学过两种交换函数的写法,分别是值传递和地址传递,但是值传递仅仅是形参的交换,它无法影响实参,故使用地址传递,现在用引用的方法也可以实现和地址传递同样的效果。示例如下:
xxxxxxxxxx
401//值传递
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.函数调用可作为左值。
示例如下:
xxxxxxxxxx
131int& 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}
xxxxxxxxxx
171int& 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++内部实现是一个指针常量
xxxxxxxxxx
101int 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修饰形参,防止形参改变实参
示例如下:
xxxxxxxxxx
231//引用使用场景,通常用来修饰形参
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.如果函数声明有默认值,函数实现的时候就不能有默认参数,声明和实现只能有一个有默认参数
xxxxxxxxxx
121//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++中函数的形参列表中可以有占位参数,用来作占位,调用函数时必须填补该位置
语法:返回值类型 函数名 (数据类型) {}
在现阶段中占位参数存在的意义不大,但是在后面的学习中会用到该技术
示例如下:
xxxxxxxxxx
131//函数占位参数,占位参数也可以有默认参数
2void func(int a ,int = 10) //对!这里没有写错,就是int = 10,当然这里也可以只写一个数据类型int
3{
4 cout << "This is a func!" << endl;
5}
6
7int main()
8{
9 func(10 ,10); //占位参数必须填补,当然有默认参数的时候可以不用填补\
10
11 system("pause");
12 return 0;
13}
作用:函数名可以相同,提高复用性
函数重载满足条件:
注意:函数的返回值不可以作为函数重载的条件
示例如下:
xxxxxxxxxx
401//函数重载需要函数在同一个作用域下,这里都是全局作用域
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}
示例如下:
xxxxxxxxxx
231//函数重载注意事项
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}
xxxxxxxxxx
201//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 类名{访问权限:属性/行为}
示例:写一个圆类,并求出圆的周长
xxxxxxxxxx
291//圆周率
2const double PI = 3.14;
3
4class Circle
5{
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}
示例:创建一个学生类
xxxxxxxxxx
361//使用字符串时需要
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}
访问权限有三种:
示例如下:
xxxxxxxxxx
291class 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的权限为public
24 //p1.m_car = "奔驰";保护权限类外访问不到
25 //p1.m_password = "1234567";私有权限,类外访问不到
26
27 system("pause");
28 return 0;
29}
不论是属性还是行为,都要遵守权限规则,定义在protected和private下的行为(函数/方法)同样无法在类外访问到。
在C++中struct
和class
的唯一区别就在于默认的访问权限不同:
struct的默认权限为公共,class的默认权限为私有
优点:
示例如下:
xxxxxxxxxx
431
2
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
~
xxxxxxxxxx
261class 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}
两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式: 括号法、显示法、隐式转换法
示例如下:
xxxxxxxxxx
431class 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++中拷贝构造函数调用时机通常有三种情况
示例如下:
xxxxxxxxxx
541class 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; //输出应该为10
33}
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的时候先发生拷贝构造,再析构对象p1
54//打印对象p的地址,析构对象p
默认情况下,C++编译器至少给一个类添加3个函数
构造函数调用规则如下:
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作
示例如下:
xxxxxxxxxx
521class 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)...{}
示例如下:
xxxxxxxxxx
161class 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的构造和析构的顺序谁先谁后?
xxxxxxxxxx
61class A{};
2
3class B{
4public:
5 A a;
6};
构造顺序为:A先,B后
析构顺序为:B先,A后
静态成员就是在成员变量和成员函数前加上关键字static
,称为静态成员。
静态成员分为:
静态成员变量
静态成员函数
示例1:静态成员变量
xxxxxxxxxx
391class 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; //输出100
21
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; //共享同一份数据,因此两行输出都是200
26
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:静态成员函数
xxxxxxxxxx
401class 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
示例如下:
xxxxxxxxxx
251class Person{
2public:
3 Person(int age)
4 {
5 //当形参和成员变量同名时,可以用this指针来区分
6 this->age = age; //写age = age;会由于无法区分形参和成员变量而出错!
7 }
8
9 Person& PersonAddPerson(Person p) //如果这里&去掉的话,返回的是值,返回的不是p2的本体,而是拷贝出的新的数据
10 { //如果这里&去掉,后续输出应该是20
11 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); //链式编程思想,应该输出40
24 cout << "p2.age" << p2.age << endl;
25}
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
示例如下:
xxxxxxxxxx
251class 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
称该对象为常对象示例如下:
xxxxxxxxxx
311class Person{
2public:
3 void showPerson() const
4 {
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
友元的三中实现
示例如下:
xxxxxxxxxx
371class Building
2{
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}
示例如下:
xxxxxxxxxx
491class goodgay
2{
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 Building
21{
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()
成员函数。
与全局函数、类做友元类似。核心代码如下:
xxxxxxxxxx
161class Building
2{
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:成员函数实现 + 号运算符重载
xxxxxxxxxx
321class 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:全局函数实现 + 号运算符重载
xxxxxxxxxx
331class 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
为例
xxxxxxxxxx
451class 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 + int
25Person 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和60
41
42 Person p4 = p1 + 100;
43 cout << "p4.m_A = " << p4.m_A << endl;
44 cout << "p4.m_B = " << p4.m_B << endl; //输出结果为110和120
45}
注意:1.对于内置的数据类型的表达式的运算符是不能改变的。2.不要滥用运算符重载。
作用:剖析cout
的本质,可以输出自定义的数据类型。
示例如下:
xxxxxxxxxx
321class 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
,这时重载左移运算符配合友元可以实现输出自定义数据类型。
作用:通过重载递增运算符,实现自己的整型数据。
示例如下:
xxxxxxxxxx
511class MyInteger
2{
3
4public:
5 MyInteger() //构造函数赋初值为0
6 {
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=
,对属性进行值拷贝如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝的问题,即堆区内存重复释放。
示例如下:
xxxxxxxxxx
431class Person
2{
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}
作用:重载关系运算符,可以让两个自定义类型的对象进行对比操作
示例如下:
xxxxxxxxxx
381class Person
2{
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 else
35 {
36 cout << "p1和p2是不相等的" << endl;
37 }
38}
示例如下:
xxxxxxxxxx
311class MyPrint
2{
3public:
4 //重载的()操作符也称仿函数
5 void operator()(string str)
6 {
7 cout << str << endl;
8 }
9};
10
11class MyAdd
12{
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种继承方式子类都不能访问到父类中的私有成员属性,公共继承,子类继承的父类的公共属性仍为公共属性、保护属性仍为保护属性;保护继承,子类继承的父类的公共属性变为保护属性、保护属性仍为保护属性;私有继承,子类继承的父类的公共属性、保护属性全部变为私有属性。
公共属性在类内和类外都可以访问到;保护属性在类内可以访问、在类外不可以访问;私有属性在类内可以访问、在类外不可以访问。
问题:从父类继承过来的成员,哪些属于子类对象中?
结论:父类中的私有成员也是被子类继承下去了,只是由于编译器给隐藏后访问不到。
利用工具(开发人员命令提示符)查看的步骤:
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
结论:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反,先析构子类,再析构父类。
问题:当子类和父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
示例如下:
xxxxxxxxxx
511class Base
2{
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 :public
23{
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
总结:
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问方式(通过对象和通过类名)
要理解两个双冒号的意义不同,例如:
xxxxxxxxxx
261//同名成员属性
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++在实际开发中不建议用多继承。
菱形继承的概念:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承就叫做菱形继承也叫钻石继承。
示例如下:
xxxxxxxxxx
241class Animal
2{
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 //虚继承后,只有一份数据,不会出现二义性,这里三个输出都为28
23
24}
注意:虚继承后继承的是一个vbptr
指针(v--virtual、b--base、prt--pointer),该指针指向虚继承表,表里面记录了到虚基类那份独一无二的成员属性的偏移量,这样就确保了孙子类里面的数据只有一份,直接访问也不会出现二义性。
总结:
多态是C++面向对象三大特性之一
多态分为两类
静态多态和动态多态的区别:
示例如下:
xxxxxxxxxx
491class Animal
2{
3public:
4 //speak函数就是虚函数
5 //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数的调用了,实现了晚绑定
6 virtual void speak()
7 {
8 cout << "动物在说话" << endl;
9 }
10};
11
12class Cat : public Animal
13{
14public:
15 void speak()
16 {
17 cout << "小猫在说话" << endl;
18 }
19};
20
21class Dog : public Animal
22{
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();
示例如下:
xxxxxxxxxx
441
2
3
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是读写的字节数
示例如下:
xxxxxxxxxx
301
2
3
4class Person
5{
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是读写的字节数
示例如下:
xxxxxxxxxx
241void 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函数,以二进制的方式读数据