面向过程编程 与 面向对象编程
什么叫做面向过程编程?
通俗的来说,这种编程风格如同从上到下,从左到右。首先,要考虑遵循的步骤,然后考虑如何表现这些数据。
那么什么又是面向对象编程呢?
从用户的角度出发考虑对象,描述对象所需的数据以及描述用户与数据进行交互所需要的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。
面向对象编程
特点:封装、继承、多态
封装:数据进行隐藏,把不想让外部使用的数据状态隐藏起来;
继承:实现代码复用;
多态:实现功能的扩展。
类和对象
抽象和类
抽象:将问题的本质抽取出来,并根据特征来描述解决方案;
类:类是一种将抽象转化为用户定义类型的C++工具,他将数据的表示和操纵的方法组合成一个整洁的包;
下面,举一个圆的例子进行说明:
#include <iostream>
using namespace std;
const int pi = 3.14;
class Circle
{
public: //类中的函数称为方法
double Calus()
{
m_s = m_r*m_r*pi;
}
void printS()
{
cout<<"面积:"<<m_s<<endl;
}
public: //类中的变量称为属性
int m_r; //半径
double m_s; //面积
};
int main()
{
Circle c; //定义了一个圆的对象c,这个c就是面向对象编程中的对象的由来
c.m_r = 2;
c.Calus();
c.printS();
return 0;
}
说明:这个圆类中包含了两个成员,半径(m_r)和面积(m_s),这两个成员就代表了圆的属性。圆类中,还有两个方法,一个求取面积,一个用来打印,都是对数据的操纵,可以看出,这些属性和方法都是在这个class Circle中,也就是说,类中包含了对象的属性、包含了对对象的操纵的函数。像这样的形式,面向对象编程。
不难发现,这种类的形式与结构体十分相像,其实在C++的内部,类也的确是由结构体进行实现的。那么这两者的区别在哪呢?
1.在C++中,结构体内部允许设置访问权限,默认访问权限是公有,而类的默认访问权限是私有。
2.定义一个结构体,可以直接用“=”进行初始化,但类只能使用构造函数进行初始化;
类的内部成员(变量和函数)访问权限:
三种:public、private、protected
public:公有属性,类的内部和外部都可以访问;
protected:保护属性,类的内部可以访问,外部不可访问;
private:私有属性,类的内部和外部都不可以访问。
构造函数和析构函数
构造函数:类的对象使用构造函数完成初始化
1.函数名与类名相同;
2.没有返回值;
3.不需要手动调用(一般情况下),系统自动调用。
析构函数:回收资源
1.函数名与类名相同,函数名前加~;
2.没有返回值;
3.回收对象资源,当对象被释放时,系统自动调用。
注意:
构造函数可以重载,析构函数不可重载
构造函数又分为无参构造、有参构造。
还是以上面的圆类进行说明:
#include <iostream>
using namespace std;
const int pi = 3.14;
class Circle
{
public: //类中的函数称为方法
Circle() //无参构造函数
{
m_r = 0;
}
Circle(int r)
{
m_r = r;
}
~Circle()
{
}
double Calus()
{
m_s = m_r*m_r*pi;
}
void printS()
{
cout<<"面积:"<<m_s<<endl;
}
private:
int m_r; //半径
double m_s; //面积
};
int main()
{
Circle c(2); //定义了一个圆的对象c,会自动调用一个参数的构造函数完成初始化
c.Calus();
c.printS();
return 0;
}
那么构造函数被调用的情形有哪些?
1.使用括号()
Circle c1; 调用无参构造
Cirlcle c2(2);调用一个参数的构造函数
2.用“=”号
Circle c1 = 4; ==> Circle c1(4)
Circle c2 = (2, 3);==> Circle c2(3) ,其后的(2, 3)其实是一个逗号表达式
3.手动调用
Circle c1 = Circle(); ==> Circle c1
Circle c2 = Circle(2);==> Circle c2(2)
拷贝构造函数:用一个对象对另外一个对象进行初始化时,系统会自动调用;
调用的情形如下:
1.Circle c1(1, 2);
Circle c2(c1); ==> Circle c2(Circle c1);
2.Circle c1(1, 2);
Circle c2 = c1; ⇒ Circle c2(c1);
3.Circle c1(1, 2);
Circle c2 = Circle(c1);
注意:
Circle c1(1, 2);
Circle c2;
c2 = c1; ??? 会调用拷贝构造函数吗?
答案是否定的,上述的形式只是赋值,并不是初始化,调用拷贝构造函数必须是初始化的时候。
4.当一个函数的形参是一个对象时,会调用拷贝构造函数
//Circle obj = c1; 因此,在参数传递时,要传对象的引用或者指针
void func(Circle obj)
{
obj.printS();
}
int main()
{
Circle c1(2);
func(c1);
return 0;
}
5.对象作为函数的返回值时
Circle func()
{
Circle c1(2);
return c1;
}
在对象作为函数返回值时,会涉及到一个匿名对象的处理,分为三种情况:
1.返回值不被接受时
2.用新对象去接收时
3.用一个已经存在的对象去接收时
默认构造函数
如果类中一个构造函数(包括任何形式的构造函数、拷贝构造函数)都没有,编译时会默认添加一个无参的构造函数,这个构造函数的函数体为空。
同样,如果类中没有析构函数,编译会默认添加一个无参的析构函数,这个析构函数的函数体为空。
提醒:析构函数的执行顺序与构造函数的执行顺序相反!!!
如果类中没有定义拷贝构造函数,编译器会默认添加一个拷贝构造函数,这个拷贝构造函数进行的是变量“值”的复制,不会复制堆上的空间。==> 浅拷贝
自己编写拷贝构造函数,实现堆上空间的复制。==> 深拷贝
下面举个例子说明浅拷贝与深拷贝的区别:
#include <iostream>
using namespace std;
class Student
{
public:
Student(int age, char *name)
{
m_age = age;
m_name = new char[20];
m_name = name;
}
~Student()
{
if(m_name != NULL)
delete []m_name;
m_name = NULL;
}
void print()
{
printf("age = %d, name = %s\n", m_age, m_name);
}
private:
int m_age;
char *m_name;
};
int main()
{
Student stu1(10, "小明");
stu1.print();
Student stu2 = stu1; //调用默认拷贝构造函数完成初始化
stu2.print();
return 0;
}
//上述代码,在执行时会有段错误发生,错误原因在于堆上的地址二次释放,下面解释一下为什么使用默认拷贝构造函数会出现这种情况。
当定义对象 stu1 时,会在堆上开辟 20 字节的空间用来存放名字,当使用 stu1 初始化 stu2 时,默认拷贝构造函数只是执行简单的“值”的复制,所以 stu2 的名字与 stu1 指向的是同一块地址,因此在析构的时候,会对 0x1000 这块地址释放两次,于是便发生了段错误。
那么为了避免这种情况的发生,需要自己写一个拷贝构造函数。
#include <iostream>
using namespace std;
class Student
{
public:
Student(int age, char *name)
{
m_age = age;
m_name = new char[20];
m_name = name;
}
~Student()
{
if(m_name != NULL)
delete []m_name;
m_name = NULL;
}
Student(const Student &obj) //自己编写的拷贝构造函数
{
m_age = obj.m_age;
m_name = new char[20];
strcpy(m_name, obj.m_name);
}
void print()
{
printf("age = %d, name = %s\n", m_age, m_name);
}
private:
int m_age;
char *m_name;
};
int main()
{
Student stu1(10, "小明");
stu1.print();
Student stu2 = stu1; //调用自己写的拷贝构造函数完成初始化
stu2.print();
return 0;
}
分析一下,为什么这样就不会出错了呢?
当定义对象 stu1 时,会在堆上开辟 20 字节的空间(0x1000)用来存放名字,当使用 stu1 初始化 stu2 时,会调用自己编写的拷贝构造函数,在函数体内,我们对 m_name 也开辟了20字节的空间(0x2000),这样两个对象的 m_name就指向不同的地址,析构时也不会出现对同一块地址重复释放的错误。