首页 >> 大全

[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了

2024-01-06 大全 36 作者:考证青年

推荐使用该方式。

// Student.h
class Student
{
private:void showMessage();public:int id;char name[32];char gender[2];};// Student.cpp
#include "Student.h"// 通过域运算符 '::' 进行成员函数的实现
void Student::showMessage()
{cout << id << name << gender << endl;
}

2.4 类的访问限定符与封装 2.4.1 封装

将数据和操作数据的方法进行有机结合,访问权限限定符用来隐藏对象的属性和细节,仅对外公开接口来和对象进行交互。

封装的本质其实是管理:将我们不想让外界知道的细节进行隐藏,开放一些公有的成员函数,对成员进行合理的访问。防止外界通过不正当方式破坏内部数据。

2.4.2 访问限定符

C++通过访问限定符进行类的封装,使用访问限定符来决定将哪些接口提供给外界访问。

注意:

2.4 类的作用域

结论:一个类就是一个作用域,在访问类中的成员是需要使用域运算符::。

class Student
{
private:void showSMessage();public:int id;char name[32];char gender[2];
};class Teacher
{
private:void showTMessage();public:int id;char name[32];char gender[2];
};// 通过域运算符 '::' 指定该成员属于哪个作用域
void Student::showSMessage()
{cout << id << name << gender << endl;
}void Teacher::showTMessage()
{cout << id << name << gender << endl;
}

2.5 类的实例化

使用类创建对象的过程,就成为类的实例化

// 定义类的代码就不看了,直接看如何实例化int main()
{// 实例化一个对象Student stu1;// 通过成员访问运算符访问类的成员stu1.id = 10;stu1.name = "张三";stu1.gender = "男";stu1.showSMessage();Student stu2, stu3; // 还可以实例化多个对象
}

2.5 类如何在内存中存储(通过计算对象的大小观察) 2.5.1 对类存储模型的三种猜想

1. 对象包含所有的成员

每新建一个对象,该对象中存储该类中的所有成员。每一个对象都是如此。

稍微思考一番,就觉得这种方法不可能,为什么?

如果每个对象都将类中的成员及成员函数存储一份,那么对象每个对象变得异常的大。如果该类有多个对象,每个对象的数据单独存储没问题,但成员函数中的代码都是相同的,如果每次都保存相同代码,造成的空间浪费是巨大的。

2. 成员函数放在类外,通过指针指向函数位置

这种存储方式貌似可行,解决了代码重用问题,并且缩小了空间。那我么写个程序测试以下,看是否与我们猜测一致。

#include 
using namespace std;class Student
{
public:void showMessage(){cout << id << "-" << name << "-" << gender << endl;}char* getName(){return name;}//private:int id;char name[32];char gender[2];
};int main()
{Student stu;stu.id = 10;strcpy(stu.name, "张三");strcpy(stu.gender, "男");stu.getName();stu.showMessage();cout << "sizeof(stu): " << sizeof(stu) << endl;cout << "sizeof(Student): " << sizeof(Student) << endl;return 0;
}

上述程序,如果按照函数只存储函数指针的方式我们猜测大小应该为 44 或者 48。但是!!!,结果是40,至此我们已经得出了结论。

3. 对象中只包含了成员变量

通过以上两种猜想,我们可以确定对象的存储模型只存储了成员变量,那么在使用时对象如何去寻找成员函数。编译器在编译时就会确定成员函数存储在何种位置,不需要指定存储位置。

2.5.2 给定一个空类,它的大小是多少

class Test
{};

由结果可知,空类所占大小为1个字节

假设空类大小为0个字节,使用空类创建了多个对象,但这些对象都没有空间,则他们在内存中就无法标识,并且在一个地方存储。就无法正确表示出这个对象。所以一定要由一个字节进行占位。

2.5.3 结论

对象的存储方式为,对象只包含成员变量,并不包含成员函数。并且按照内存对齐的方式进行存储。大小计算也与结构体的计算方式相同。空类占1个字节。

3. this指针 3.1 问题提出

1.在类中,为什么可以把成员变量放在成员函数下方定义,并且可以正常使用?

2.多个对象调用一个函数,该函数怎么区分对哪个对象操作?

3.2 this指针的作用

在上述的问题2中,如何多个对象调用一个函数,如何区分是哪个对象调用?

通过this指针解决该问题。即在编译期间C++编译器,对每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体中所有的成员变量操作,都是通过这个指针去访问。只不过不需要用户对该指针进行传递,编译器自动完成。

在成员函数中可以通过 this->XXX 访问成员变量

class Student
{
public:Student(int id, const char name[], const char gender[]){_id = id;strcpy(_name, name);strcpy(_gender, gender);}void showMessage(){cout << "this address: " << this << endl;cout << _id << "-" << _name << "-" << _gender << endl;// 也可以通过这样访问cout << this->_id << this->_name << endl;}private:int _id;char _name[32];char _gender[2];
};int main()
{Student stu1(1, "张三", "男");cout << "&stu1 = " << &stu1 << endl;stu1.showMessage();cout << endl;Student stu2(2, "李四", "女");cout << "&stu2 = " << &stu2 << endl;stu2.showMessage();return 0;
}

结果:

3.3 this指针的特性

	void showMessage(/*Student* this*/){cout << "this address: " << this << endl;cout << _id << "-" << _name << "-" << _gender << endl;// 也可以通过这样访问cout << this->_id << this->_name << endl;}

4. 默认的成员函数 4.1 类的6个默认成员函数

如果一个类中什么成员都没有,则称该类空空类。但是空类中真的什么都没有吗?在我们声明一个类时,不管我们是否向类中添加成员,都会生成下列的6个默认的成员函数。

4.2 构造函数 4.2.1 作用及用法

在类中,由于其中的成员变量具有封装的特性,所以无法直接对其中的成员赋值,所以要通过设置公有方法进行对私有属性的改变,这样很麻烦。在对象创建时就把对应的信息设置进去。

所以,构造函数就出现了,构造函数较特殊,函数名与类名相同,并且无返回值,创建类类型对象时由编译器自动调用,并且在该对象的声明周期中只会调用一次。如果没有定义构造函数,则编译器会自动生成一个无参的构造函数。

没有构造函数的对成员进行设值 每次新建对象都这样这样操作

class Stu
{
public:void SetId(int _id){id = _id;}void SetAge(int _age){age = _age;}private:int id;int age;
};int main()
{// 给对象设置值Stu s1;s1.SetAge(10);s1.SetId(1111);return 0;
}

利用构造函数设值 显然方便了许多

class Stu
{
public:Stu(int _id, int _age){id = _id;age = _age;}private:int id;int age;
};int main()
{// 新建对象时设值Stu s1(10, 123); Stu s2(11, 124);Stu s3(13, 125);return 0;
}

[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了__[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了

4.2.2 特性

1.构造函数的重载

注意:一旦自己改写了,构造函数,则编译器不会再生成默认的构造函数

如果有时候不需要对新建的对象设值,那么上面的方式就达不到我们的要求,但是构造函数的另一特性又出现了,可以对构造函数进行重载,满足编程时的不同要求。

class Stu
{
public:// 两个参数的构造函数Stu(int _id, int _age){id = _id;age = _age;}// 无参的构造函数Stu() {}// 一个参数的构造函数Stu(int _id){id = _id;}private:int id;int age;
};int main()
{// 利用重载创建不同对象Stu s1;Stu s2(1, 123);Stu s3(1);return 0;
}

2. 用户如果定义构造函数则不会自动生成无参构造函数

class Stu
{
public:// 两个参数的构造函数Stu(int _id, int _age){id = _id;age = _age;}private:int id;int age;
};int main()
{Stu s1;  // err 因为找不到无参的构造函数Stu s2(1, 123);return 0;
}

3. 无参构造函数和全缺省的构造函数都是默认构造函数,只能有一个

class Stu
{
public:// 全缺省构造函数Stu(int _id = 1, int _age = 10){id = _id;age = _age;}// 无参构造函数Stu() {}private:int id;int age;
};int main()
{Stu s1;		// 产生了二义性Stu s2(1, 123);return 0;
}

4. 如果类嵌套类,则在生成类对象时,会自动调用嵌套类的无参构造函数

5. 在构造函数中是对成员的赋值,不是对成员的初始化

4.3 初始化列表

在上述构造函数内部,我们给成员变量赋值,在那一部分,不是对成员变量的初始化,因为初始化只有一次,而函数体内部的赋值可以有多次。如果对成员变量进行初始化,就利用到了现在的初始化列表。

4.3.1 初始化列表的使用

class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id)		// 初始化列表以 ':' 开始, age(_age)		// 以 ',' 进行分隔{}private:int id;int age;
};

4.3.1 注意事项

1.类中包含以下成员则必须在初始化列表位置进行初始化

1.1 const成员变量

因为const成员变量无法在声明时进行赋值,但是const具有不可修改的特性,但是不能没有初始值,所以就要在初始化列表的位置进行初始化。

class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id), age(_age), status(1)		// 对const成员变量进行初始化{}private:int id;int age;// const成员变量const int status;
};

1.2 引用成员变量

原因上同

class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id), age(_age), ref(_age){}private:int id;int age;// int& ref;
};int main()
{//Stu s1;		// 产生了二义性Stu s2(1, 123);return 0;
}

1.3 没有默认构造函数的自定义类型

全缺省和无参构造函数都被称为默认构造函数

如果自定义类型有默认的构造函数,再创建母对象时,会调用自定义类型的无参构造函数。若没有默认的构造函数则必须要给类型传参数,否则无法成功建立对象,所以要在初始化列表的位置进行自定义类型的初始化。

class Test
{
public:Test(int _score): score(_score){}private:int score;
};class Stu
{
public:Stu(Test _test, int _id = 1, int _age = 10): id(_id), age(_age), test(_test)  // 自定义类型初始化{}private:int id;int age;//自定义类型的成员 Test test;
};

1.4 成员变量的初始化顺序,是在类中的声明顺序,与初始化列表顺序无关

class Test
{
public:// 初始化列表的初始化顺序:// b -> c -> a// 因为先用b初始化的a,但a中为随机值,所以b中也是随机值Test(int _a = 1, int _b = 2, int _c = 3): a(_a)	, b(a), c(_c){}void PrintRes(){cout << a << " " << b << " " << c << endl;}private:int b;int c;int a;	// 注意这里 a 的位置
};int main()
{Test test;test.PrintRes();return 0;
}

4.4 析构函数 4.4.1 功能

析构函数的功能与构造函数功能恰恰相反,析构函数不是完成对象的销毁,而是完成对象中资源的清理的工作,对象在销毁时,会自动调用对象的析构函数,完成对象中资源的清理。如果在构造对象时,没有申请资源,则可以不去写析构函数。

class Stu
{
public:Stu(int _id, const char* _name): id(_id){cout << "构造函数" << endl;name = (char*)malloc(sizeof(char) * 16);strcpy(name, _name);}// 析构函数// 因为在构造阶段进行了资源的申请,则在对象销毁期间必须释放资源~Stu(){cout << "析构函数" << endl;free(name);}void Print(){cout << "id:" << id << " name:" << name << endl;}private:int id;char* name;
};int main()
{Stu s(1, "张三");s.Print();return 0;
}

4.4.2 特性 4.5 拷贝构造函数 4.5.1 功能

在创建对象时,通过已有对象,创建出与其完全相同的新对象,称为拷贝构造函数。

拷贝构造函数,无返回值,只有单个形参,该形参是对本类类型的常引用,用已经存在的对象创建新对象,由编译器自动调用。

函数原型

class Test
{Test(const Test& test){// 函数体}
};

示例:

class Stu
{
public:// 构造函数Stu(int _id = 1, int _score = 2){id = _id;score = _score;}void Print(){cout << "id:" << id << " socre:" << score << endl;}private:int id;int score;
};int main()
{Stu s1(1, 100);s1.Print();// 调用系统的默认拷贝构造函数Stu s2(s1);s2.Print();// 输出结果完全相同return 0;
}

4.5.2 特征

1. 如果构造函数中有申请资源的行为,那么不可使用默认拷贝构造函数

这个程序会崩溃,为什么?

class Stu
{
public:// 构造函数Stu(int _id, const char* _name){id = _id;name = (char*)malloc(sizeof(char) * 16);strcpy(name, _name);}void Print(){cout << "this:" << this << "  this.name:" << &(this->name) << endl;cout << "id:" << id << " socre:" << name << endl;}// 析构函数 释放资源~Stu(){cout << "析构函数" << endl;free(name);}private:int id;char* name;
};int main()
{Stu s1(1, "张三");s1.Print();// 调用系统的默认拷贝构造函数Stu s2(s1);s2.Print();return 0;
}

通过上述程序可以看出,通过拷贝构造函数构造出的对象,从对象地址可以看出不是同一个对象,但是对象中的name的空间是通过动态申请得到的,系统默认的拷贝构造函数,在拷贝时,不会去申请地址空间,而是直接复制前一个对象的地址,共用一一份资源。

在销毁对象时,会调用析构函数,析构函数中对申请的资源进行了释放,因为共用一份资源,所以会对一块空间释放两次,造成了程序的崩溃。

2. 拷贝函数的参数只有一个且必须使用引用传参,使用传值则会发生无限递归

假设拷贝构造函数为传值

在参数传递时,需要生成一份实参的拷贝,因为是自定义类型,所以在生成实参的拷贝时,会调用拷贝构造函数。该过程又是一个拷贝实参的过程,所以又会调用拷贝构造,就这样一直递归下去。

4.5.3 什么时候会调用拷贝构造函数 4.6 运算符重载 4.6.1 功能

但我们自定义了一个类型,系统原本的操作符无法满足我们的操作。例如,定义了一个日期类,比较两日期大小,通过系统 < 或 >无法比较自定义类型,可以写一个函数进行比较,但是可读性较差,所以引入运算符重载。

C++为了增强代码可读性引入了运算符重载,运算符重载时具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表。不能改变原有运算符的含义。

函数名:关键字 后接需要重载的运算符符号

函数原型:返回值类型 操作符(参数列表)

4.6.2 运算符重载的注意事项 4.6.3 赋值运算符重载 ‘=’ (重要)

例如,新建了两个对象,两个对象内容不同,如何使两个对象中的内容相同,那么就要通过赋值运算符将两个对象中的内容变得相同。

class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print(){cout << "id:" << id << " socre:" << score << endl;}// 赋值运算符重载Stu& operator=(const Stu& s){if(this != s){cout << "重载= 被调用" << endl;id = s.id;score = s.score;}return *this;}private:int id;int score;
};int main()
{Stu s1(1, 100);s1.Print();Stu s2(2, 40);s2.Print();// 调用赋值运算符重载s1 = s2;s1.Print();s2.Print();return 0;
}

注意

1.如果没有写赋值运算符的重载,系统会默认生成,如果没有申请资源,效果与上述代码中效果相同。

2.如果构造函数中申请了资源,则要注意不能直接使用默认的赋值运算符重载,负责会出现与拷贝构造函数相同的情况。

3.重载’=‘运算符,不能改变原来的含义,比如说 = 原来可以连序赋值,所以该运算符必有返回值。

4.6.4 取地址运算符重载

这个运算符一般不需要重载,除非想要特殊的输出才去进行重载。

如果对象被const修饰,会不会调用 '&'的重载?

class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() {cout << "id:" << id << " socre:" << score << endl;}Stu* operator&(){cout << "重载& 被调用" << endl;return this;}private:int id;int score;
};int main()
{const Stu s1(1, 100);s1.Print();const Stu* s2 = &s1;return 0;
}

因为const修饰的变狼是不能被修改的,防止在方法中对该const修饰的对象进行修改,所以const修饰的变量是不能调用非const修饰的方法的。

修改成这样才可以正常被调用
const Stu* operator&() const
{cout << "重载& 被调用" << endl;return this;
}

4.6.5 其他运算符的重载示例

由于代码片段过长,在此贴一个连接,想了解的兄弟们,可以点进去看看。

点这里: gitee

5. const成员 5.1 const修饰成员变量

const修饰的成员变量具有不可更改性。const修饰的变量是不能通过其他手段修改的。

在C语言中的const可以通过指针去简介的修改const的值,但是在c++中不可以。C语言中const是一个不能修改的变量,而C++中const是一个常量。

// C++中
const int a = 10;
int arr[a];  // ok

const int a = 10;
int* pa = (int*)&a;
*pa = 100;cout << a << endl;  // 输出结果为 10 

c++中的const修饰的常量具有宏替换的属性,在编译期间就已经确定了const所修饰的值。

5.2 const修饰成员函数 5.2.1 概念及使用

概念

如果const修饰成员函数,将该成员函数称为const成员函数,const不能修饰普通函数指针修饰成员函数。

被const修饰的成员函数,实际是修饰该成员函数的隐含 this指针,表明该函数在成员函数中不能对类的成员做任何修改。

写法

const int Test(const int a) const

第一个const 修饰函数的返回值

第二个const 修饰函数的参数,在函数体内不能改变形参

第三个const 修饰函数本身:

本质是在修饰隐含的this指针const Test* const

在这个函数中,this所指向的成员变量不能修改,可以读取成员中的内容。

注意

如果想在const成员函数中修改某个成员变量,在变量声明前加上 关键字,可以在const成员函数中修改该变量。

class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() const{id = 10;cout << "id:" << id << " socre:" << score << endl;}private:mutable int id;int score;
};

5.2.2 对取地址运算符的重新重载

使用const修饰

class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() const{id = 10;cout << "id:" << id << " socre:" << score << endl;}const Stu* operator&() const{cout << "重载& 被调用" << endl;return this;}private:mutable int id;int score;
};int main()
{const Stu s1(1, 100);s1.Print();const Stu* s2 = &s1;return 0;
}

6. 成员 6.1 概念与初始化

声明为的类成员称为类的静态成员,用修饰的成员变量,称为静态成员变量,用修饰的成员函数,称为静态成员函数。

例如,要统计这个类中共创建了多少个对象?

class Test
{
public:Test(){++count;}Test(const Test& t){++count;}~Test(){--count;}// 定义成员函数static int getCount(){a = 10; // err  不能访问非静态成员return count;}private:// 定义静态成员变量 所有成员共享static int count;int a;
};// 初始化静态成员变量
int Test::count = 0;int main()
{Test t1;Test t2(t1);t2 = t1;// 通过如下三种方式都可以正常的访问静态成员函数,说明静态成员函数与具体的某个对象无关。// 是所有对象共享的函数cout << t1.getCount() << endl;cout << t2.getCount() << endl;cout << Test::getCount() << endl;return 0;
}

6.2 修饰成员变量与成员函数的特性

不能修饰构造、拷贝构造、赋值运算重载、析构、虚函数。

6.2.1 修饰成员变量 6.2.1 修饰成员函数 7. 友元

通过友元声明的类和函数,可以直接访问类中的私有成员,同时,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部进行声明,声明时要加上关键字。

7.1 友元函数 7.1.1 友元函数的引入

1. 问题的提出

自定义了一个类型,如果想利用内置关键字cout对该自定义对象输出,就需要对

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了