1、面向过程与面向对象
1、面向过程
面向过程是一种以时间为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把在这些步骤实现,在一步一步的具体步骤中再按顺序调用函数
2、面向对象
在日常生活或者编程中,简单的问题可以用面向过程的思路来解决,直接有效,但是当问题的规模变得更大时,用面向过程的思想是远远不够的,所以慢慢就出现了面向对象的编程思想,世界上有和很多人和事物,每一个都可以看做一个对象,而每个对象都有直接的属性和行为,对象与对象之间通过方法来交互。面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目标的目的不是为了完成一个步骤,而是为了描述某个对象在整个解决问题的步骤中的属性和行为。
2、类与对象
概念:
对象:对象是人们要进行研究的任何事务,它不仅能表示具体的事务,还能表示抽象的规则、计划或事件。
对象的载体:人、电脑、风扇、桌子、游戏、旅游、棋盘系统。。。
对象的描述:特征/属性 --- 变量 --- 数据成员
功能/方法 --- 函数 --- 函数成员
类:具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。因此,对象的抽象就是类,类的具体化(实例化)就是对象,类实际上就是一种数据类型。(但从语法层面讲,类其实就是增加了函数的结构体)
在c++中,用户定义类型指的是实现抽象接口的类设计。
设计和使用一个类
1)找出对象 归类 --- 时间类
2)抽象:(提炼共同点)
特征/属性:时 分 秒
功能/方法:显示时间 设置时间
3)封装
把对象的属性和服务结合成一个独立的系统单元,尽可能屏蔽对象的内部细节,对外形成一个边界(或者说一道屏障),只保留有限的对外接口使之与外部发生联系
4)实例化对象
定义一个类的对象,通过对象调用类中的成员
类的简单设计格式
class 类名
{
//类的特征(属性) 成员变量 权限一般为保护或私有
//类的行为(功能) 成员方式,函数 权限一般为公有
public://公共权限 公有段的成员时提供给外部的接口
protected://保护权限
private://私有权限
};
public:公有权限 类的外部,内部,派生类,友元都可以访问
private:私有权限 类的内部,派生类,友元都可以访问
protected:保护权限 类的内部,友元都可以访问
将属性设置为私有权限的好处:
1、可以自己控制属性的读写权限,达到数据的安全性
2、可以检查数据的正确性
类定义,一般来说,类规范由两个部分组成。
1、类声明:以数据成员的方式描述数据部分,以成员函数(称为方法)的方式描述公有接口
2、类方法定义:描述如何实现类成员函数
简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。
什么是接口
接口是一个共享框架,供两个系统交互式使用。对于类,我们说公共接口。在这里,公共是使用类的程序,交互系统由类对象组成而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。
如果希望更人性化,不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户,然而要使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口。
通常c++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中(封装的一种例子)。多文件实现的时候,定义成员函数文件中,使用作用域解析运算符来表示函数所属的类,需要删去类的关键字和成员变量。声明类的文件中,不需要使用作用域解析运算符,关键字和成员变量也保留。在程序员编写代码中,有一种常见但不通用的约定---将类名首字母大写。
结构体和类有什么区别呢?
结构体:成员默认公有 --- 对外开放
类:成员默认私有 ---不对外开放,需要用接口访问成员
成员的性质由关键字public,protected,private决定,如果在进行类的声明时不写上面这些关键字,默认都是私有的
访问控制
使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(友元函数)来访问对象的私有成员。共有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口,防止程序直接访问数据被称为数据隐藏。类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。
访问类的成员
1、类内部的成员访问其他成员,直接访问
2、类的外部访问类的成员,通过对象才能访问
类的实例化
1)静态分配内存
2)动态分配内存
类的成员内部声明,外部定义
3、类的内存空间大小
1、类本身是一种数据类型,在没有定义对象前是不占用内存空间的,定义对象的时候才会分配空间
2、计算一个类的对象占用多少空间用sizeof(类名或对象)
1)类的内存空间大小是其数据成员(非静态-数据段)和虚表大小有关,跟函数成员无关
2)如果一个类中没有数据成员(空类),也没有虚表,那么这类的大小规定为1个字节
3、为什么空类的大小为一个字节
实际上,这是类结构体实例化的原因,空的类或结构体同样可以被实例化,如果定义对空的类或者结构体取sizeof()的值为0,那么该空的类或结构体实例化出很多实例时,在内存地址上就不能区分该类实例化出的实例,所以,为了实现每个实例在内存中都有一个独一无二的地址,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无的地址,所以空类所占的内存大小是一个字节,通俗一点将就是用空类实例化一个对象,这个对象的尸体要存在内存中,既然要存在内存中,那么就需要有个地址能访问他,为了避免多个实例对象的地址问题,所以才有了一个字节的空间
#include <iostream>
using namespace std;
class Data
{
public:
//成员函数
private:
//属性
//空类的内存大小为1
int m_a; //前缀(m_) 或 后缀(_)
};
int main()
{
Data d1;
cout << "sizeof(Data):" << sizeof(Data) << endl;
return 0;
}
4、构造函数
1、概念
函数名与类名相同,函数没有返回值,函数不需要用户调用,在创建对象的时候自动调用
主要作用:在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
最好是在创建对象时对它进行初始化,c++为此提供了一个特殊的成员函数----类构造函数,专门用于构造新对象,将值赋给它们的数据成员。
2、格式
class 类名
{
public:
类名(形参) //构造函数,一般放在public权限下
{}
};
1、构造函数没有返回值,也不写void
2、函数名称与类名相同
3、构造函数可以有参数,因此可以发生函数重载(创建对象的时候根据不同的参数调用不同的构造函数)
4、构造函数支持带默认参数(默认参数在声明和定义的时候,其中一个写上就可以了)
注意:当实例化对象不带参数的时候,可能会跟带默认参数的构造函数发生歧义
5、程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
创建对象的方式:
1、类名 对象名
2、类名 对象名(参数)
3、类名* 对象名 = new 类名(参数)
4、类名 对象名 = 类名(参数) ----匿名对象
5、类名 对象名 = 参数 ----显示写法
3、参数列表
在创建对象的时候,一般在构造函数里面初始化成员变量,除了可以使用这种方法之外,还可以通过参数列表的方式初始化
概念:参数列表主要是初始化成员变量,或者调用父类的构造方法(后期继承的时候使用)
格式:
class 类名
{
public:
类名(数据类型 参数1,数据类型 参数2):成员变量1(初始化值),成员变量2(初始化值)
private:
数据类型 成员变量1;
数据类型 成员变量2;
}
如果构造函数的定义是在类的外面定义,那么参数列表初始化是写在函数定义,声明不需要写
const修饰类的数据成员(相当于常量)必须在构造函数的初始化列表中初始化
构造函数内部初始化与参数列表初始化区别
构造函数内部初始化 必须要等对象创建完才能调用,而参数列表初始化是在创建对象的时候就执行了(也就是定义空间的时候立即初始化了),在后期继承的时候父类构造方法,以及const修饰的常量多要用参数列表初始化
6、析构函数
概念:函数名与类名相同在函数名前面添加~,函数没有返回值,没有参数,当对象销毁的时候系统自动调用(可以在析构函数中释放成员空间)
格式:
class 类名
{
public:
~类名()
{
//存放资源释放的语句
}
}
1、析构函数,没有返回值也不写void
2、函数名称与类名相同,在名称前加符号~
3、析构函数不可以有参数,不可以发生函数重载
4、程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include <iostream>
using namespace std;
class Data
{
public:
//成员函数
//构造函数(函数重载)
// Data()
// {
// cout << "Data()" << endl;
// }
// Data(int a)
// {
// this->m_a = a;
// cout << "Data()" << endl;
// }
// Data(int a = 10) //默认参数
// {
// this->m_a = a;
// cout << "Data()" << endl;
// }
Data(int a):m_a(a) //参数列表化
{
this->m_a = a;
cout << "Data()" << endl;
}
void show()
{
cout << "m_a:" << this->m_a << endl;
}
//析构函数
~Data()
{
cout << "~Data()" << endl;
}
private:
//属性
int m_a; //前缀(m_) 或 后缀(_)
};
int main()
{
//Data d1(20);
Data d1();
d1.show();
return 0;
}
7、拷贝构造函数
概念:拷贝构造函数,它只有一个参数,参数类型是本类的引用。如果类的设计者不写拷贝构造函数,编译器就会自动生成拷贝构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,让目标对象的每个成员变量都会变得和源对象相等。编译器自动生成的拷贝构造函数称为“默认拷贝构造函数。
格式:
格式:
class 类名
{
//拷贝构造函数,参数是本类的引用
//如果没有定义,那么编译器就自动生成默认的拷贝构造函数
类名(const 类名 &a){}
}
什么时候需要自己定义拷贝构造函数
当类的数据成员中有指针成员的时候,需要申请内存空间
什么时候系统会自动使用拷贝构造函数
在定义一个对象通过另一个对象来初始化,那么会调用拷贝构造函数
深拷贝和浅拷贝的区别
浅拷贝:只拷贝对象本身空间里面的内容,编译器默认生成的拷贝构造函数就是浅拷贝
浅拷贝在数据成员中开辟堆区变量的情况中,会发生堆区内存重复释放而发生错误,从而导致程序崩溃
深拷贝:拷贝对象本身空间内容的同时,还要分配成员指向的堆空间并且进行拷贝
#include <iostream>
#include <cstring>
using namespace std;
class Data
{
public:
//成员函数
Data(int a = 10,const char* name = "sakura0908") //默认参数
{
this->m_name = new char[20];
this->m_a = a;
strcpy(this->m_name,name);
cout << "Data()" << endl;
}
//拷贝默认函数
#if 0 //编译会出现下面第一张照片的问题
//浅拷贝,会有堆区内存空间重复释放的危险
Data(const Data& other)
{
this->m_a = other.m_a;
strcpy(this->m_name,other.m_name);
cout << "Data(const Data& other)" << endl;
}
#endif
//深拷贝
Data(const Data& other)
{
this->m_name = new char[20];
this->m_a = other.m_a;
strcpy(this->m_name,other.m_name);
cout << "Data(const Data& other)" << endl;
}
void show()
{
cout << "m_a:" << this->m_a << endl;
cout << "m_name:" << this->m_name << endl;
}
//析构函数
~Data()
{
cout << "~Data()" << endl;
}
private:
//属性
int m_a; //前缀(m_) 或 后缀(_)
char* m_name;
};
int main()
{
Data d1;
d1.show();
Data d2(d1);
d2.show();
return 0;
}
8、this指针
有时候方法可能涉及到两个对象,在这种情况下需要使用c++的this指针
this指针指向用来调用成员函数的对象(this被称为隐藏参数传递给方法),一般来说,所有的类方法都将类方法三个字设置为调用它的对象的地址。
注意:每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this,在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。然而要返回的并不是this,因为this是对象的地址,而不是对象本身,即*this(将解除引用运算符*用于指针,将得到指针指向的值)。
this指针的用途
本质:指针常量
1、当形参和成员变量同名时,可用this来区分
2、在类的非静态成员函数中,可使用return *this
9、默认构造函数
默认构造函数是在未提供显式初始化值时,用来创建对象的构造函数。
如果没有提供任何构造函数,则c++将自动提供默认构造函数。它是默认构造函数的隐式版本,默认构造函数没有参数,因为声明中不包含值。但奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。在设计类时,通常应提供对所有类成员隐式初始化的默认构造函数。
默认情况下,至少给c++的类添加3个函数
1、默认构造函数(无参)
2、默认析构函数(无参)
3、默认拷贝函数(浅拷贝)
构造函数调用规则如下:
1、如果用户定义有参构造函数,c++不会再提供默认无参构造函数,但是会提供拷贝构造函数
2、如果用户定于拷贝构造函数,c++不会再提供其他构造函数
10、类的组合
概念:一个类的对象作为另外一个类的数据成员
也就是说,两个及以上相互独立的类能够放在一起,然后通过一个类就可以调用另一个类的对象从而调用另一个类的功能
构造函数调用顺序
1、在构造函数的列表中指定内嵌对象的构造函数的形式
2、如果不指定内嵌对象构造函数的形式,则调用的默认构造函数
3、顺序:
内嵌对象 ===》 本类
多个内嵌对象
顺序:按照定义内嵌对象的先后顺序调用内嵌对象的构造函数
析构函数调用顺序
顺序:本类 ===》 内嵌对象
#include <iostream>
using namespace std;
class DataB
{
public:
//构造函数
DataB()
{
cout << "DataB()" << endl;
}
//析构函数
~DataB()
{
cout << "~DataB()" << endl;
}
};
class DataC
{
public:
//构造函数
DataC()
{
cout << "DataC()" << endl;
}
//析构函数
~DataC()
{
cout << "~DataC()" << endl;
}
};
class DataA
{
public:
//构造函数
DataA()
{
cout << "DataA()" << endl;
}
//析构函数
~DataA()
{
cout << "~DataA()" << endl;
}
private:
DataB b;
//DataC c;
};
int main()
{
DataA a;
return 0;
}
11、const和static修饰类
1、static修饰类的数据成员
总结:
1、静态变量存储在数据段中,空间大小不包含在类的对象大小中
2、类的静态成员必须在类外初始化,不初始化在使用的时候就会报错
3、类的静态数据成员是先于类对象存在,如果数据是公有的可以直接通过类名访问
4、使用的时候公有静态成员,可以直接通过类名访问
5、同一个类的不同对象,静态数据成员是共享的(内存空间下是共享的)
2、static修饰类的函数成员
使用static修饰的类函数成员,称之为静态函数成员
1、不能在静态函数里面使用this指针
2、不能在静态函数中使用非静态成员(保护普通数据成员和普通函数成员)
3、静态函数先于对象存在,所以此时通过类名取调用静态函数,普通的数据成员此时还没有内存空间
4、可以在普通函数中使用静态成员
5、类外实现的时候不需要加关键字static
总结:
static修饰类的函数成员,不能在该函数中使用this指针,也不能使用非静态数据成员
static修饰类的成员函数先于类的对象存在,如果是公有静态函数可以在类的外部直接使用
3、const修饰类的成员变量---常量
用途:创建对象后就不需要改变的数据就可以用const修饰
const修饰成员变量在构造函数参数列表初始化
4、const修饰类的成员函数
用途:用在查询的接口(访问成员数据,不能修改数据)
只能调用const修饰的成员函数
const修饰的函数中不能修改数据成员变量,只能访问
const修饰的是this指针
总结:
1、类的成员函数由const修饰,其实const修饰的是this指针,那么在这个函数中 不能修改成员变量的数据,同时只能调用由const修饰的成员函数
2、如果const关键字修饰成员函数在类内声明类外实现,那么声明和定义都需要写上关键字
5、const修饰对象 --- 常对象
const修饰对象,那么这个对象只能访问const修饰的成员函数,不能访问非const成员函数