学习总结
(1)构造函数可以有多个重载形式;在实例化对象时,即使有多个构造函数,也仅用到其中的一个构造函数;当用户没有定义构造函数时,编译器将自动生成一个构造函数。
(2)拷贝构造函数:定义格式:类名(const
类名 & 变量名)
首先要加一个const
关键字,其次传入的是一个引用,这个引用还是一个与自己的数据类型完全相同(比如,一个Student
的一个对象)的引用。
(3)初始化列表先于构造函数执行;只能用于构造函数;可同时初始化多个数据成员。
(4)构造函数的分类:
按照参数分类:有参和无参(默认构造函数)构造;
按照类型分类:普通构造和拷贝构造。
文章目录
- 学习总结
- 一、封装篇(上)
- 1.1 类和对象、类对象定义
- 1.2 初始字符串类型
- (1)string类型
- (2)初始化string对象的方式
- (3)string的常用操作
- (4)代码实践
- 1.3 属性封装的艺术
- (1)数据的封装
- 通过函数来封装数据成员:
- (2)封装的好处
- (3)代码实践
- 1.4 类内定义与类外定义
- (1)类内定义
- 类内定义与内联函数的关系
- (2)类外定义
- 1.同文件类外定义
- 2.分文件类外定义(重点)
- 1.5 对象的构造
- (1)对象结构
- (2)对象初始化
- (3)构造函数
- 1)无参构造函数
- 2)有参构造函数
- (4)构造函数代码实践
- (5)默认构造函数
- (6)构造函数初始化列表
- 1)特点
- 2)必要性(牛逼处)
- (7)初始化列表代码实践
- (8)拷贝构造函数
- 1)定义格式
- (9)构造函数小结
- 1.6 对象的析构
- (1)析构函数
- 1)析构函数的必要性
- 2)析构的特点
- (2)对象的生命历程
- (3)析构函数代码实践
一、封装篇(上)
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 name's length is:"<<name.size() <<endl;
cout<<"Your name's 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{
public:
Student(){//默认构造函数
cout<<"Student"<<endl;
}
private:
string m_strName;
};
int main(){
Student stu1;
Student stu2=stu1;
//法一:下面是用了括号法
Student stu3(stu1);
//法二:如果用显示法,则上面:Student stu3 = Student(stu1)
return 0;
}
因为第2、3个实例化对象调用的不是上面定义的默认构造函数,而是一种特殊的构造函数——拷贝构造函数。
注意:不用利用拷贝构造函数 初始化匿名对象,如下面第二行,编译器会认为Person(p3)
等价于Person p3
,即认为是一个对象声明(最后会重定义报错):
Person p3 = Person(p2);
Person(p3);
构造函数的隐式转换法:
Person p4 = 10;//相当于写了 Person p4 = Person(10);这种显示法的有参构造
Person p5 = p4;//拷贝构造函数
1)定义格式
定义格式:类名(const
类名 & 变量名)
即把一个对象的属性拷贝到当前对象身上。
注意:要加上const
,不能把人家的本体对象修改了。
class Student{
public:
Student(){//默认构造函数
m_strName="Keiven";
}
Student(const Student &stu){}//拷贝构造函数
private:
string m_strName;
};
首先要加一个const
关键字,其次传入的是一个引用,这个引用还是一个与自己的数据类型完全相同(也就是说,也是一个Student
的一个对象)的引用。
通过这样的定义方式,我们就定义出了一个拷贝构造函数。
如果我们将相应的代码写在拷贝构造函数的实现的部分,那么我们再采用上面两种实例化对象的方式,就会执行拷贝构造函数里面的相应代码。
在实例化stu2
和stu3
的时候,我们并没有去定义拷贝构造函数,但是仍然可以将这两个对象实例化出来。
可见,拷贝构造函数与普通的构造函数一样。
(1)如果没有自定义拷贝构造函数,则系统会自动生成一个默认的拷贝构造函数。
(2)当采用直接初始化或复制初始化实例化对象时,系统自动调用拷贝构造函数(如引例)。
(9)构造函数小结
(1)所有的无参数构造函数都是默认构造函数。
(2)系统会自动生成一些函数——分为普通构造函数 and 拷贝构造函数。
如果自定义了普通构造函数,系统就不会再自动生成默认构造函数;
如果自定义了拷贝构造函数,系统就不会再自动生成拷贝函数。
(3)初始化列表,只能连接在普通构造函数或者拷贝构造函数的后面。
PS:显然拷贝构造函数不能重载(参数是确定的,类-型)。
1.6 对象的析构
(1)析构函数
在对象销毁时会被自动调用,归还系统的资源。
(因其没有参数,所以析构函数就不能重载;另外注意析构函数没有返回值)
定义格式:~类名()
class Student{
public:
Student(){
cout<<"Student"<<endl;
}
~Student(){//不允许加任何参数
cout<<"~Student"<<endl;
}
private:
string m_strName;
};
1)析构函数的必要性
class Student{
public:
Student(){
m_pName=new char[20];
}
private:
char *m_pName;
};
以Student
学生类,对于数据成员姓名,我们不用string
型,而是使用字符数组。
我们改用指针,并且在其构造函数中,让这个指针指向堆中分配(new
出来的)的一段内存;
在该对象销毁时,就必须释放掉这段内存,否则会造成内存泄漏。
要释放内存,最好的时机是对象被销毁之前,如果销毁早了则其他要用到这些资源的程序会报错。
so->设计一个对象销毁之前被自动调用的函数就非常有必要(即析构函数)。
2)析构的特点
(1)如果没有自定义析构函数,系统就会自动生成。
(2)析构函数在对象销毁时被自动调用(相对的是:构造函数,即在对象实例化时被自动调用)。
(2)对象的生命历程
(3)析构函数代码实践
定义一个Teacher类,自定义析构函数;
对于普通方式实例化的对象,在销毁对象时是否自动调用析构函数;(会)
对于拷贝构造函数实例化的对象,在销毁对象时是否自动调用析构函数;(会)
头文件:
#include<iostream>
#include<string>
using namespace std;
class Teacher{
public:
Teacher(string name="Keiven",int age=20);
//申明有参 默认构造函数,带有初始值
Teacher(const Teacher &tea);//申明拷贝构造函数
~Teacher();
void setName(string name);
string getName();
void setAge(int age);
int getAge();
int getMax();
private:
string m_strName;
int m_iAge;
};
主函数:
#include"Teacher.h"
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
/* ************************************************************************/
/* 定义一个Teacher类,具体要求如下:
/* 1、自定义析构函数
/* 2、对于普通方式实例化的对象,在销毁对象时是否自动调用析构函数
/* 3、通过拷贝构造函数实例化的对象,在销毁对象时是否自动调用析构函数
/* 数据成员:
/* 名字
/* 年龄
/* 成员函数:
/* 数据成员的封装函数
/* ************************************************************************/
Teacher::Teacher(string name,int age):m_strName(name),m_iAge(age){
//初始化列表形式定义有参构造函数
cout<<"Teacher(string name,int age)"<<endl;
}
//拷贝构造函数
Teacher::Teacher(const Teacher &tea){
cout<<"Teacher(const Teacher &tea)"<<endl;
}
//析构函数
Teacher::~Teacher(){
cout<<"~Teacher()"<<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(t1);//通过拷贝构造函数实例化对象
Teacher *p=new Teacher();//在堆上实例化一个对象
delete p;
system("pause");
return 0;
}
结果如下:
并且因为上面代码加上了system("pause");
,我们在黑框上按下任意键后一瞬间其实有执行实例化对象的析构函数,即打印出如下效果:
- 对于普通方式实例化的对象,在销毁对象时是否自动调用析构函数;(会)
- 对于拷贝构造函数实例化的对象,在销毁对象时是否自动调用析构函数;(会)