C++面向对象之封装篇(上)
Posted wx62cea850b9e28
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++面向对象之封装篇(上)相关的知识,希望对你有一定的参考价值。
学习总结
(1)构造函数可以有多个重载形式;在实例化对象时,即使有多个构造函数,也仅用到其中的一个构造函数;当用户没有定义构造函数时,编译器将自动生成一个构造函数。
(2)拷贝构造函数:定义格式:类名(const
类名 & 变量名)
首先要加一个const
关键字,其次传入的是一个引用,这个引用还是一个与自己的数据类型完全相同(比如,一个Student
的一个对象)的引用。
(3)初始化列表先于构造函数执行;只能用于构造函数;可同时初始化多个数据成员。
(4)构造函数的分类:
按照参数分类:有参和无参(默认构造函数)构造;
按照类型分类:普通构造和拷贝构造。
文章目录
- 学习总结
- 一、封装篇(上)
- 1)定义格式
一、封装篇(上)
1.1 类和对象、类对象定义
3.1.1 引例----狗
class Dog//class为关键字
char name[20];
int Age;//属性
int type;//数据成员
void spead();//方法
void run();//成员函数
3.1.2 引例----电视机
class TV
public:
char name[20];//电视名字
int type;//电视类型
void changeVol();//调节音量
void power();//开关电源
;
3.1.3 对象的实例化
(1)从栈实例化
(使用完后,系统会自动将其所占内存释放)
int main()
TV tv;//定义一个对象
TV tv[10];//定义一个对象数组
return 0;
(2)从堆实例化
(使用完后,要我们自己释放内存)
int main()
TV *p=new TV();//定义一个对象
TV *q=new TV[10];//定义一个对象数组
//对象使用完后,需要释放内存
delete p;
delete []q;//删除指向对象数组的指针
return 0;
3.1.4 对象成员的访问
通过不同实例化对象的方法,生成的对象访问其数据成员和成员函数的方式也不同。
(1)栈实例化对象访问其成员
(2)堆实例化单一对象访问其成员
(3)堆实例化对象数组访问其成员
3.1.5 代码实践
#include<iostream>
#include<stdlib.h>
using namespace std;
class Coordinate
public:
int x;
int y;
void printX()
cout<<x<<endl;
void printY()
cout<<y<<endl;
;
int main()
Coordinate coor;//从栈中实例化一个坐标对象
coor.x=10;
coor.y=20;
coor.printX();
coor.printY();
Coordinate *p=new Coordinate();//从堆中实例化一个坐标对象
//这里需要判断申请的内存是否成功
if(NULL==p)
cout<<"申请内存失败"<<endl;
return 0;
p->x=100;
p->y=200;
p->printX();
p->printY();
//对象使用完后,需要释放内存
delete p;
p=NULL;
system("pause");
return 0;
输出结果为:
1.2 初始字符串类型
(1)string类型
字符串,如果只用char数组,经常使用strlen、strcat、strcmp函数会比较麻烦,而C++就因此引入string
类型。
string hobby="football";
cout<<hobby<<endl;
(2)初始化string对象的方式
string s1;//s1为空字符串
string s2("ABC");//用字符串字面值初始化s2
string s3(s2);//将s3初始化为s2的一个副本
string s4(n,c);//将s4初始化为字符‘c’的n个副本
(3)string的常用操作
s.empty() //若s为空串,则返回true
s.size()//返回s中字符的个数
s[n]//返回s中位置为n的字符,位置从0开始
s1+s2//将两个串连接成新串,返回新生成的串
s1=s2//将s1的内容替换为s2的副本
(4)代码实践
题目描述:
1 提示用户输入姓名
2 接收用户的输入
3 然后向用户问好,hello xxxx
4 告诉用户名字的长度
5 告诉用户名字的首字母是什么
6 如果用户直接输入回车,那么告诉用户输入的为空
7 如果用户输入的是imooc,那么告诉用户的角色是一个管理员
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
int main()
string name;
cout<<"please input your name: ";
getline(cin, name);
if(name.empty())
cout<<"input is null..."<<endl;
system("pause");
return 0;
if(name == "imooc")
cout<<"You are a administartor"<<endl;
cout<<"hello " + name <<endl;
cout<<"Your names length is:"<<name.size() <<endl;
cout<<"Your names first letter is: "<< name[0] <<endl;
system("pause");
return 0;
1.3 属性封装的艺术
(1)数据的封装
如果按照C语言的面向过程的习惯,我们会这样写:
class Student
public:
string name;
int age;
;
int main()
Student stu;
stu.name=Jim;
stu.age=10;
return 0;
而面向对象:以对象为中心,要以谁做什么来表达程序的逻辑,
代码层上:将所有的数据操作转换为成员函数的调用,对象在程序中所有的行为都通过自己的函数完成。
通过函数来封装数据成员:
class Student
private:
void setAge(int _age)
age=_age;
int getAge()
return age;
private:
string name;
int age;
;
即上面的2个成员函数:一个设置年龄的值,一个读取年龄的值。
(2)封装的好处
(1)防止非法输入
如上面的年龄防止输入1000这种不合理的数据。
class Student
private:
void setAge(int _age)
if(_age>0 && _age<100)
age=_age;
else
.....
int getAge()
return age;
private:
string name;
int age;
;
(2)可以控制外界对数据的访问属性
如Car
类,我们不希望外界通过某个函数改变m_iWheelCount
变量的值,即只希望设置为只读属性,那就设置为private
。
class Car
public:
int getWheelCount()
return m_iWheelCount;
private:
int m_iWheelCount;
;
(3)代码实践
定义一个Student类,含有如下信息:
1 姓名:name
2 性别:gender
3 学分(只读):score
4 学习:study(用于获得学分)
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
/* **********数据的封装
定义一个Student类,含有如下信息:
姓名:name
性别:gender
学分(只读):score
学习:study(用于获得学分)
/* ************************************/
class Student
public:
void setName(string _name)
m_strName=_name;
string getName()
return m_strName;
void setGender(string _gender)
m_strGender = _gender;
string getGender()
return m_strGender;
int getScore()
return m_iScore;
void initScore()
m_iScore=0;
void study(int _score)
m_iScore += _score;
private:
string m_strName;
string m_strGender;
int m_iScore;
;
int main()
Student stu;
stu.initScore();//如果这里不进行初始化,m_iScore的值就不可控
stu.setName("Zhangsan");
stu.setGender("女");
stu.study(5);//学习一门学分为5分的课程
stu.study(3);//学习一门学费为3分的课程
cout<<stu.getName()<<" "<<stu.getGender()<<" "<<stu.getScore()<<endl;
system("pause");
return 0;
1.4 类内定义与类外定义
(1)类内定义
将成员函数的函数体写在类的内部。
类内定义与内联函数的关系
类内定义的成员函数,编译器会将其优先编译为内联函数,
但对于复杂的成员函数无法编译成内联函数的,就编译成普通的函数。
普通函数和内联函数的区别:
上面的步骤即体现了内联函数是省掉了(2)和(4)步骤,可以为调用节省很多时间(尤其对于循环调用时省时)。
(2)类外定义
即将成员函数的函数体写在类的外面。
分为:同文件类外定义、分文件类外定义
1.同文件类外定义
注意开头写上类名和::
2.分文件类外定义(重点)
几乎所有的C++项目都会将类的定义分文件完成。
头文件,类名建议和文件名写成一样。
class Car
public:
void run();
void stop();
void changeSpeed();
;
在另一个文件的开头记得将头文件Car.h
写上:
#include "Car.h"
void Car::run()
void Car::stop()
void Car::changeSpeed()
1.5 对象的构造
学习前的思考:
(1)实例化的对象是如何在内存中存储的?
(2)类中的代码又是如何存储的?
(3)数据和代码之间的关系是咋样的?
(1)对象结构
内存中按照用途被划分的5个区域
int x=0;int *p=NULL;//存储在栈
int *p=new int[20];//存储在堆区,注意这里的数组名为p,而不是int(关键字int)
存储全局变量和静态变量//全局区
string str="hello";//常量区
存储逻辑代码的二进制//代码区
栈区:内存由系统进行控制(程序员不需要关心其分配和回收)
堆区:new
分配一段内存(会分配在堆区),需要我们自己用delete
回收这段内存。
全局区:全局变量、静态变量
常量区:字符串、常量
代码区:存储编译后的二进制代码
定义一个Car类,在类被实例化前是不会用到堆or栈中的内存的,
class Car
private:
int wheelCount;
public:
int getWheelCout()
return wheelCount;
但当它实例化后,如实例化出car1、car2、car3对象,每个对象都存储会在【栈】中(不同对象在栈中位置不同),但是逻辑代码却只编译出一份(放在代码区)。
当需要的时候,这个代码区中的代码供所有的对象使用。
(2)对象初始化
上面的实例化三个对象后,每个对象中的数据都不可控(因为没有对数据初始化)。
栗子:
定义坦克类,只描述位置,实例化一个坦克对象t1后,通过t1调用初始化函数(init)。
class Tank
private:
int m_iPosX;
int m_iPosY;
public:
void init()
m_iPosX=0;
m_iPosY=0;
;
int main()
Tank t1;
t1.init();
Tank t2;
t2.init();
return 0;
对象的初始化:可能有且仅有一次 or 根据条件初始化多次。
(3)构造函数
有时需要有且仅有一次的初始化操作,为了防止0次或者多次调用初始化函数,C++推出构造函数。
构造函数:在对象实例化时被自动调用(仅被调用一次)。
相关规则:
(1)构造函数与类同名;
(2)没有返回值,连void
都不用写;
(3)可以有多个重载形式;
(4)在实例化对象时,即使有多个构造函数,也仅用到其中的一个构造函数;
(5)当用户没有定义构造函数时,编译器将自动生成一个构造函数。
1)无参构造函数
class Student
public:
Student()
m_strName="Keiven";
private:
string m_strName;
;
在Student类中,构造函数没有任何返回值,构造函数内部对数据成员进行赋值操作(初始化)。
2)有参构造函数
class Student
public:
Student(string name)
m_strName=name;
private:
string m_strName;
;
这种构造函数即在用户实例化一个Student对象时可以传进一个name作初始值。
(4)构造函数代码实践
先定义头文件,注意下面的类里的构造函数参数中age=40是在设置默认参数——如果没有传参年龄,即只传参了名字也会直接调用第二个构造函数,而年龄默认为40(设置默认参数)。
头文件:
#include<iostream>
#include<string>
using namespace std;
class Teacher
public:
Teacher();//申请无参构造函数
//Teacher(string name,int age);//申请有参构造函数
Teacher(string name,int age=40);
void setName(string name);
string getName();
void setAge(int age);
int getAge();
private:
string m_strName;
int m_iAge;
;
主函数如下:
#include"Teacher.h"
#include<stdlib.h>
#include<iostream>
#include<stdio.h>
using namespace std;
/* ************************************************************************/
/* 定义一个Teacher类,具体要求如下:
/* 自定义无参构造函数
/* 自定义有参构造函数
/* 数据成员:
/* 名字
/* 年龄
/* 成员函数:
/* 数据成员的封装函数
/* ************************************************************************/
//自定义无参构造函数
Teacher::Teacher()
m_strName="Keiven";
m_iAge=20;
cout<<"Teacher()"<<endl;
//自定义有参构造函数
Teacher::Teacher(string name,int age)
m_strName=name;
m_iAge=age;
cout<<"Teacher(string name,int age)"<<endl;
void Teacher::setName(string _name)
m_strName=_name;
string Teacher::getName()
return m_strName;
void Teacher::setAge(int _age)
m_iAge=_age;
int Teacher::getAge()
return m_iAge;
int main()
Teacher t1;//调用无参构造函数
Teacher t2("Mery",30);//调用有参构造函数
Teacher t3("James");
//检查上面3个构造函数是否有初始化数据成员
cout<<t1.getName()<<" "<<t1.getAge()<<endl;
cout<<t2.getName()<<" "<<t2.getAge()<<endl;
cout<<t3.getName()<<" "<<t3.getAge()<<endl;
system("pause");
输出结果为:
注意:
构造函数除了可以重载,还可以给参数赋值默认值,
但不能随意的赋值默认值,有时会引起编译时不通过——因为实例化对象时,编译器不知道调用哪一个构造函数。
(5)默认构造函数
默认构造函数:在实例化对象时,不需要传递参数的构造函数。
注意:调用默认构造函数时,不要加()
,否则编译器会认为是一个函数的声明,不会认为在创建对象。
class Student
public:
Student()
Student(string name="Keiven");
private:
string m_strName;
;
int main()
Student stu1;
Student *p=NULL;
p=new Student();
实例化2个对象:一个是stu1(从堆中实例化对象),
另一个是用p指针指向实例化的一个对象所在的栈空间(从堆中实例化一个对象)。
无论是从栈中还是堆中实例化对象,共同特点:调用的构造函数都不用传参——如上面代码有两种情况都不用传参:
Student()
Student(string name="Keiven");
(6)构造函数初始化列表
在这个例子中,我们定义了一个学生Student的类,在这个类中,我们定义了两个数据成员(一个名字,一个年龄),名字和年龄都通过初始化列表(即红色标记部分)进行了初始化。姓名和年龄都通过初始化列表进行初始化
注意
(1)构造函数后需要冒号隔开,多个数据成员之间则是用逗号隔开
(2)赋值用括号赋值,而非等号
class Student
public:
Student():m_strName("Keiven"),m_iAge(30)
private:
string m_strName;
int m_iAge;
;
1)特点
(1)初始化列表先于构造函数执行
(2)初始化列表只能用于构造函数
(3)初始化列表可同时初始化多个数据成员
2)必要性(牛逼处)
(1)会报错的栗子:
在这个例子中,我们定义了一个圆Circle的类,在这个类中定义了一个pi值,因为pi是不变的,我们用const来修饰,从而pi就变成了一个常量。我们如果用构造函数来初始化这个常量(像上面那样),这样的话编译器就会报错,而且会告诉我们,因为pi是常量,不能再给它进行赋值。也就是说,我们用构造函数对pi进行赋值,就相当于第二次给pi赋值了。那如果我们想给这个pi赋值,并且又不导致语法错误,怎么办呢?唯一的办法就是通过初始化列表来实现(如下)。这个时候,编译器就可以正常工作了。
class Circle
public:
Circle()m_dPi=3.14;
private:
const double m_dPi;
;
原因:const
修饰的m_dPi
是常量,即编译器会因为不能给他进行赋值而报错(二次赋值),而唯一办法是通过初始化列表解决。
(2)不报错(解决)
class Circle
public:
Circle()m_dPi=3.14;
private:
const double m_dPi;
;
(7)初始化列表代码实践
描述:定义一个Teacher类,自定义有参默认构造函数,适用初始化列表初始化数据;数据成员包含姓名和年龄;成员函数为数据成员的封装函数;拓展:定义可以带最多学生的个数,此为常量。
Teacher.h
头文件:
#include<iostream>
#include<string>
using namespace std;
class Teacher
public:
Teacher(string name="Keiven",int age=20,int m=100);
//申明有参默认构造函数,带有初始值
void setName(string name);
string getName();
void setAge(int age);
int getAge();
int getMax();
private:
string m_strName;
int m_iAge;
const int m_iMax;//常量(只能通过初始化列表进行初始化
;
源文件:
#include"Teacher.h"
#include<iostream>
#include<stdlib.h>
using namespace std;
/* ************************************************************************/
/* 定义一个Teacher类,具体要求如下:
/* 自定义有参默认构造函数
/* 适用初始化列表初始化数据
/* 数据成员:
/* 名字
/* 年龄
/* 成员函数:
/* 数据成员的封装函数
/* 拓展:
/* 定义可以带最多学生的个数,此为常量
/* ************************************************************************/
Teacher::Teacher(string name,int age,int m):m_strName(name),m_iAge(age),m_iMax(m)
//初始化列表形式定义有参构造函数
cout<<"Teacher(string name,int age)"<<endl;
int Teacher::getMax()
return m_iMax;
void Teacher::setName(string _name)
m_strName=_name;
string Teacher::getName()
return m_strName;
void Teacher::setAge(int _age)
m_iAge=_age;
int Teacher::getAge()
return m_iAge;
int main()
Teacher t1("Mery",30,150);
//看2个构造函数是否完成数据成员的初始化
cout<<t1.getName()<<" "<<t1.getAge()<<" "<<t1.getMax()<<endl;
system("pause");
(8)拷贝构造函数
实例化三个对象,本以为会打印出3个student
,而结果只打印了一个:
class Student
<以上是关于C++面向对象之封装篇(上)的主要内容,如果未能解决你的问题,请参考以下文章