29 面向对象的程序设计
来自:https://www.cctry.com/thread-289448-1-1.html
面向过程:
C语言就是面向过程的语言
面向过程的程序中函数是构成程序的基本单位,我要实现个功能我就新写一个函数,我要实现另外一个功能我就再新写一个函数,之后调用就行了
面向对象:
万物都是对象
任何一个对象都应该有属性和行为这两个要素。一个对象就是由多个属性和多个行为构成的。
面向对象的程序设计:将这个程序涉及到的方方面面分成不同的彼此间有联系的多个对象。
在C++语言中,每个对象都是由数据和函数这两个部分构成的。
数据就是对象的属性,函数就是对象的行为或者功能,用来对数据进行操作的,以便实现某些功能。
对象的封装与信息屏蔽
① 将这个对象相关的属性和行为封装在对象中,集成在对象中,形成一个基本单位,各个对象之间相互独立,互不干扰。
② 对象中的某些实现细节对外界屏蔽,隐藏内部实现的细节,只提供基本可用的函数接口,让外界调用。利于数据的安全。
抽象
抽象就是归类,或者说分类
C++中,类就是对象的抽象,而对象就是类的特例,即,类的具体表现形式。
继承与重用
多态性
面向对象编程中的多态性,主要指的是对同一类别的不同对象调用同一个行为或者函数的时候,表现不同。
在C++中多态性指的是由继承而产生的不同的派生类,派生类对象对同一行为调用会做出不同的响应。
31 类的成员函数
来自:https://www.cctry.com/thread-289700-1-1.html
inline 内联函数:
调用普通函数会有较大开销
对于短小、相对简单又调用频繁的函数:
C语言中可以用宏来实现
#define MAX_NUM(x, y) (x > y ? x : y) //声明定义
int ret = MAX_NUM(3, 6); //调用
宏跟函数的区别是,在编译阶段就将宏的代码展开直接替换调用宏的地方。所以省去了函数调用的压栈、出栈等开销。所以执行效率方面要比函数高。
但是代码的可阅读性会变差,不便维护
C++中引入inline内联函数:在调用的时候,将函数的代码直接嵌入到调用的地方
备注1:默认情况下,在类体中直接定义/实现的函数(函数体写在类里面的),C++会自动的将其作为inline内联函数来处理。
而在类体外部定义的函数C++则会将其作为普通的类的成员函数来处理。
备注2:如果函数的执行体很大,很耗时,就不适合作为 inline 内联函数。
只有当函数的执行体很小,只有几行代码,而且会被频繁的调用的时候才适合作为 inline 内联函数的。
32 this指针
成员函数的存储方式:
C++语言中每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,而不包括函数代码所占用的存储空间。
this指针:
既然成员函数不占用对象的存储空间,或者说多个对象是共用一个成员函数的,那么问题就来了。在调用成员函数的时候,函数如何区分是哪一个对象调用的呢?
用this指针
this指针是指向本类对象的指针,
它的值是当前被调用的成员函数所在的对象的起始地址。
所以,当对象调用成员函数的时候,如:zhang_san.print_name();
则系统会把 zhang_san 对象的地址赋值给 this 指针,
所以在 print_name 函数的内部调用 cout 打印 name 成员的时候,实际上就是 this->name 这样调用的。只不过这里面的 this 可以省略不写,默认就是调用当前对象的。
void Student::print_name()
{
cout << "name = " << this->name <<endl;
}
什么情况下需要手动加上this:
如下例:成员变量和函数传过来的实参的名字一样时,在成员变量前加this->
void set_age(int age)
{
this->age = age;
};
33 类的构造函数
构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来主动调用它,
构造函数会在对象被建立时自动被调用的。作用就是用来处理对象的初始化操作。
构造函数的注意事项:
①、构造函数的名字必须与类名同名,不能随意命名,这样的话才能让编译器认为该函数是构造函数,而不是类的普通成员函数;
②、构造函数不具有任何类型,不返回任何值,连 void 都不是;没有返回值
③、构造函数不需要用户调用,也不应该被用户调用,它是对象建立的时候自动被系统调用,用来初始化刚刚建立的对象的;
④、如果用户没有定义自己的类的构造函数,那么系统会自动生成一个默认的构造函数,只不过该构造函数的函数体是空的,也就是什么都不做。
构造函数可以带参数,如果要用带参数的构造函数,在实例化对象时后面加()写初始值
小作业:
有这么个需求,所有的在校生都必须有学号,所以能不能实现实例化一个Student对象之后,就要求输入学号,不存在一个没有学号的Student对象被实例化出来。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string name;
int num;
char sex;
int age;
Student(int n):num(n) //使用了初始化列表
{
num = n;
}
};
int main()
{
//Student stu1; //错误,类Student不存在默认构造函数
Student stu2(01);
return 0;
}
构造函数初始化列表:
https://blog.csdn.net/weixin_45550460/article/details/104031464
34 函数的重载与默认参数
https://www.cctry.com/thread-289930-1-1.html
函数默认参数的注意事项:
①、在函数声明的时候指定,如果没有函数的声明则可以放在函数的定义中,但是声明和定义只能选一个;
②、从第一个有默认值的参数开始,后面的所有参数必须都有默认值才行;
③、调用的时候如果要自定义非第一个默认值的参数,那么前面所有的参数都要明确的写上默认值才行;
④、从使用角度来说函数的默认值比重载更方便,从函数内部实现角度来说比函数的重载更复杂。
小作业:
实现一个函数,该函数的声明如下:
bool string_upper_diy(char str[], int str_len, bool b_odd_pos = true);
功能为对字符串指定位置的字符变换为大写。
str 参数为字符串的指针;
str_len 为字符串的长度;
b_odd_pos 为true的时候,就要将 str 字符串中的奇数位置的字符变为大写,同时将非奇数位置的字符变为小写;
b_odd_pos 为false的时候,就要将 str 字符串中的奇数位置的字符变为小写,同时将非奇数位置的字符变为大写;
奇数指的是:1,3,5,7,9,11,13,15,17,19…以此类推;
#include <iostream>
#include <string>
using namespace std;
bool string_upper_diy(char str[], int str_len, bool b_odd_pos = true)
{
if (b_odd_pos == true)
{
//将 str 字符串中的奇数位置的字符变为大写,同时将非奇数位置的字符变为小写
for (int i = 1; i <= str_len; i++)
{
if (str[i - 1] >= 'a' && str[i - 1] <= 'z' && (i%2 != 0) )
{
str[i - 1] = str[i - 1] - ('a' - 'A'); //小写转大写,减32 ASCII码值:'a' 97 'A' 65
}
if (str[i - 1] >= 'A' && str[i - 1] <= 'Z' && (i % 2 == 0))
{
str[i - 1] = str[i - 1] + ('a' - 'A'); //大写转小写,加32
}
}
return true;
}
if (b_odd_pos == false)
{
//将 str 字符串中的奇数位置的字符变为小写,同时将非奇数位置的字符变为大写
for (int i = 1; i <= str_len; i++)
{
if (str[i - 1] >= 'A' && str[i - 1] <= 'Z' && (i % 2 != 0))
{
str[i - 1] = str[i - 1] + ('a' - 'A');
}
if (str[i - 1] >= 'a' && str[i - 1] <= 'z' && (i % 2 == 0))
{
str[i - 1] = str[i - 1] - ('a' - 'A');
}
}
return false;
}
}
int main()
{
char s[10];
cin >> s;
string_upper_diy(s, 5, false);
for (int i = 0; i < 5; i++)
{
cout << s[i] << " ";
}
cout << endl;
return 0;
}
35 类的构造函数与析构函数
析构函数的作用:
构造函数不是新建对象党的,而是在对象被创建出来之后自动被调用,用来初始化相关信息的函数。
同理,析构函数也不是用来删除对象的,而是当对象被删除的时候自动会被调用的,用来做一些对象被删除之前的清理工作。
只要对象的生命周期结束,那么程序就会自动执行析构函数来完成这个工作的。
析构函数的特点:
析构函数不返回任何值,没有函数类型,也没有任何函数的参数。
由于上面这些特点,所以析构函数不能被重载,
所以说一个类可以有多个构造函数,但只能有一个析构函数。
小作业:
void test()
{
CStudent zhang_san("zhangsan", 'f', 1001, 21);
CStudent li_si("lisi", 'm', 1002, 22);
CStudent wang_wu("wangwu", 'm', 1003, 23);
}
在 test 函数中我依次定义了三个局部变量的对象:zhang_san、li_si、wang_wu,那么当离开 test 函数之后,这三个对象的析构顺序又是怎么样的呢?
#include <iostream>
#include <string>
using namespace std;
class CStudent
{
private:
string name;
char sex;
int num;
int age;
public:
CStudent(string n, char s, int m, int a)
{
name = n;
sex = s;
num = m;
age = a;
cout << "CStudent(string name, char sex, int nun, int age) " << name << endl;
}
~CStudent()
{
cout << "~CStudent() " << name << endl;
}
};
void test()
{
CStudent zhang_san("zhangsan", 'f', 1001, 21);
CStudent li_si("lisi", 'm', 1002, 22);
CStudent wang_wu("wangwu", 'm', 1003, 23);
}
int main()
{
test();
return 0;
}
36 对象的赋值与复制及拷贝构造函数
对象赋值的注意事项:
①、对象的赋值,只是对对象的成员变量的赋值,对于成员函数来说不理会,也不做赋值处理,因为本身每个类的成员函数就一份而已所以也不需要赋值。(之前说过,成员函数不属于任何一个对象,是所有对象公共使用的)
②、类似上面的矩形类 CMyRect 的对象是可以直接用=等号赋值的,但是有一些成员是没办法用=等号赋值的,例如,成员变量是其他一种类对象,那么此时的解决办法就是重载类的=等号运算符,这个以后会给大家讲解;
③、还有一种情况下,使用对象的赋值也是非常危险的,如下例:
#include <iostream>
#include <string>
using namespace std;
class CStudent
{
public:
char* p_name;
char sex;
int num;
int age;
public:
CStudent() {};
CStudent(const char* t_name, char t_sex, int t_num, int t_age);
~CStudent();
};
CStudent::CStudent(const char* t_name, char t_sex, int t_num, int t_age) :sex(t_sex), num(t_num), age(t_age)
{
int n_len = strlen(t_name);
p_name = new char[n_len + 1];
memset(p_name, 0, n_len + 1);
strcpy_s(p_name, n_len + 1, t_name);
}
CStudent::~CStudent()
{
if (p_name)
{
delete[] p_name;
}
}
void test()
{
CStudent stud_1("zhangsan", 'f', 1001, 20),stud_2;
stud_2 = stud_1;
}
int main()
{
test();
return 0;
}
错误原因:
stud_1在构造的时候也完成了初始化,stud_2构造完成后也进行了初始化,只不过它调用的是默认构造函数,而默认构造函数并没有给它赋初始值。所以对象stud_2的p_name指针还是个野指针
当执行完stud_2 = stud_1;
后,会进行对象的赋值,把stud_1的各变量的值拷贝给stud_2,包括p_name指针的值,此时stud_1和stud_2这两个对象里面的p_name指针都指向了同一块内存
stud_1和stud_2的作用域都在test函数里,当超过作用域后,这两个对象都会进行释放,(即他们的生命周期结束)此时就会调用析构函数。当stud_1进行析构时,析构函数对stud_1的p_name指针指向的内存进行清空,释放资源内存。当stud_2再调用析构函数,也释放p_name指针指向的内存时,(注意,stud_1和stud_2这两个对象里面的p_name指针的是同一块内存),就相当于同一块内存又要释放一次,肯定会出错的
对象的复制:
对象的赋值,是对已经存在的两个对象进行操作。对象的复制是一个对象从无到有的一个过程,在对象创建的时候就以一个已经存在的对象为源头进行创建本对象。
Student zhangsan = {"zhangsan", 1002, 20};
Student lisi(zhangsan);
Student wangwu = lisi;
复制/拷贝构造函数:
以下这个就是拷贝构造函数(参数是对象的引用):
Student(Student& stud)
{
strcpy(this->name, stud.name);
this->num = stud.num;
this->age = stud.age;
}
拷贝构造函数的特点:
①、也是构造函数,所以函数名字就是类名字,并且无返回值;
②、参数上有点特殊,参数是一般是本类的对象的引用;
③、如果类没有提供拷贝构造函数,那么编译器会默认生成一个拷贝构造函数,作用比较单一,只是简单的将成员变量的值进行复制过去而已。
拷贝构造函数实现的必要性:
如果一个类没有提供拷贝构造函数,那么编译器会默认生成一个。既然编译器都可以默认生成了,我们为什么还要实现呢?
①、类的成员变量中有一些无法进行赋值的,此时就需要自定义实现拷贝构造函数;
②、类似上面使用对象的赋值第③种情况,像那种用指针的,用默认的拷贝构造函数也会出错