学习目标:
了解类和对象的基本概念
学习内容:
1.类和对象的基本概念
C语言使用结构化程序设计:
程序 = 数据结构 + 算法
程序由全局变量以及众多相互调用的函数组成。
算法以函数的形式实现,用于对数据结构进行操作。
结构化程序设计的不足:
结构化程序设计中,函数和其所操作的数据结构,没有直观的联系。
随着程序规模的增加,程序逐渐难以理解,很难一下子看出来:
某个数据结构到底有哪些函数可以对它进行操作?
某个函数到底是用来操作哪些数据结构的?
任何两个函数之间存在怎样的调用关系?
结构化程序设计没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充。
难以查错,当某个数据结构的值不正确时,难以找出到底是那个函数导致的。
重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。
在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。
总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充(增加新功能),难以查错,难以重用。
软件业的目标是更快、更正确、更经济地建立软件。
• 如何更高效地实现函数的复用?
• 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。
面向对象的程序设计:
面向对象的程序设计方法,能够较好解决上述问题。
面向对象的程序 = 类 + 类 + …+ 类
设计程序的过程,就是设计类的过程。
面向对象的程序设计方法:
将某类客观事物共同特点(属性)归纳出来,形成一个数据结构(可以用多个变量描述事物的属性);
将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)。
然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装”。
面向对象的程序设计具有“抽象”,“封装”“继承”“多态”四个基本特点。
从客观事物抽象出类:
写一个程序,输入矩形的长和宽,输出面积和周长。
比如对于“矩形”这种东西,要用一个类来表示,该如何做“抽象”呢?
矩形的属性就是长和宽。因此需要两个变量,分别代表长和宽。
一个矩形,可以有哪些行为呢(或可以对矩形进行哪些操作)?
矩形可以有设置长和宽,算面积,和算周长这三种行为(当然也可以有其他行为)。
这三种行为,可以各用一个函数来实现,他们都需要用到长和宽这两个变量。
将长、宽变量和设置长,宽,求面积,以及求周长的三个函数“封装”在一起,就能形成一个“矩形类”。
长、宽变量成为该“矩形类”的“成员变量”,三个函数成为该类的“成员函数” 。 成员变量和成员函数统称为类的成员。
实际上,“类”看上去就像“带函数的结构”。
#include<bits/stdc++.h>
#include<iostream>
#include<string>
using namespace std;
class CRectangle//跟结构体很像,其实这里写struct也没什么事
{
public://等会再说
int w, h ;//宽和高(成员变量)
//三个成员函数
int Area(){
return w*h;
}//算面积的
int Perimeter(){
return 2 * (w+h);
}//算周长的
void Init( int w_,int h_){
w = w_;h=h_;
}//用来初始化宽和高的
};
int main(){
int w ,h ;
CRectangle r;//r是一个对象
cin >> w >> h ;
r.Init(w,h);
cout << r.Area() << endl <<r.Perimeter();
cout << "hello" << endl;
return 0;
}
通过类,可以定义变量。类定义出来的变量,也称为类的实例,就是我们所说的“对象” 。
C++中,类的名字就是用户自定义的类型的名字。可以象使用基本类型那样来使用它。CRectangle 就是一种用户自定义的类型。
对象的内存分配:
和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和,不包含成员函数。
对于上面的CRectangle类,sizeof(CRectangle) = 8
每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象。
对象之间的运算:
和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。
使用类的成员变量和成员函数:
用法1:对象名.成员名
CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4);
用法2. 指针->成员名
CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上
用法3:引用名.成员名
CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了,r2的值也变
void PrintRectangle(CRectangle & r)
{ cout << r.Area() << ","<< r.Perimeter(); }
CRectangle r3;
r3.Init(5,4);
PrintRectangle(r3);
类的成员函数和类的定义分开写:
class CRectangle
{
public:
int w,h;
int Area();
int Perimeter();
void Init( int w_,int h_);
};
//类的成员函数和类的定义可以分开写
int Crectangle::Area(){
return w * h;
}
int Crectangle::Perimeter(){
return 2* (w + h);
}
void Crectangle::Init(int w_,int h_){
w = w_; h= h_;
}
Crectangle::说明后面的函数是Crectangle类的成员函数,而非普通函数。那么一定要通过对象或对象的指针或对象对的引用才能调用。
类成员的可访问范围:
在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:
– private: 私有成员,只能在成员函数内访问
– public : 公有成员,可以在任何地方访问
– protected: 保护成员,以后再说
以上三种关键字出现的次数和先后次序都没有限制。
定义一个类:
class className {
private: //私有属性和函数
public://公有属性和函数
protected: //保护属性和函数
}
如过某个成员前面没有上述关键字,则缺省地被认为是私有成员。
class Man {
int nAge; //私有成员
char szName[20]; // 私有成员
public:
void SetName(char * szName){
strcpy( Man::szName,szName);
}
};
在类的成员函数内部,能够访问:
– 当前对象的全部属性、函数;
– 同类其它对象的全部属性、函数。
在类的成员函数以外的地方,只能够访问该类对象的公有成员。
class CEmployee {
private:
char szName[30]; //名字
public :
int salary; //工资
void setName(char * name);
void getName(char * name);
void averageSalary(CEmployee e1,CEmployee e2);
};
void CEmployee::setName( char * name) {
strcpy( szName, name); //ok
}
void CEmployee::getName( char * name) {
strcpy( name,szName); //ok
void CEmployee::averageSalary(CEmployee e1,CEmployee e2){
cout << e1.szName; //ok,访问同类其他对象私有成员
salary = (e1.salary + e2.salary )/2;
}
int main()
{
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;
}
int main()
{
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;
}
设置私有成员的机制,叫“隐藏”
“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。
如果将上面的程序移植到内存空间紧张的手持设备上,希望将szName 改为 char szName[5],若szName不是私有,那么就要找出所有类似
strcpy(e.szName,"Tom1234567889");
这样的语句进行修改,以防止数组越界。这样做很麻烦。
如果将szName变为私有,那么程序中就不可能出现(除非在类的内部)
strcpy(e.szName,"Tom1234567889");
这样的语句,所有对 szName的访问都是通过成员函数来进行,比如:
e.setName( “Tom12345678909887”);
那么,就算szName改短了,上面的语句也不需要找出来修改,只
要改 setName成员函数,在里面确保不越界就可以了。
struct CEmployee {
char szName[30]; //公有!!
public :
int salary; //工资
void setName(char * name);
void getName(char * name);
void averageSalary(CEmployee
e1,CEmployee e2);
};
和用"class"的唯一区别,就是未说明是公有还是私有的成员,就是公有。
成员函数的重载及参数缺省:
成员函数也可以重载
成员函数可以带缺省参数。
#include <iostream>
using namespace std;
class Location {
private :
int x, y;
public:
void init( int x=0 , int y = 0 );
void valueX( int val ) { x = val ;}
int valueX() { return x; } //成员函数的重载
};
void Location::init( int X, int Y)
{
x = X;
y = Y;
}
int main() {
Location A,B;
A.init(5);
A.valueX(5);
cout << A.valueX();
return 0;
}
输出:
5
!!使用缺省函数一定要注意避免有函数重载时的二义性
class Location {
private :
int x, y;
public:
void init( int x =0, int y = 0 );
void valueX( int val = 0) { x = val; }
int valueX() { return x; }
};
Location A;
A.valueX(); //错误,编译器无法判断调用哪个valueX
2.构造函数
基本概念:
成员函数的一种
名字与类名相同,可以有参数,不能有返回值(void也不行)
作用是对对象进行初始化,如给成员变量赋初值
如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数
•默认构造函数无参数,不做任何操作
如果定义了构造函数,则编译器不生成默认的无参数的构造函数
对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
一个类可以有多个构造函数
为什么需要构造函数?
- 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
- 有时对象没被初始化就使用,会导致程序出错。
下面写了一个构造函数的例子:
class Complex {
private :
double real, imag;//代表复数,他有实部和虚部两个变量
public:
void Set( double r, double i);
}; //编译器自动生成默认构造函数
Complex c1; //默认构造函数被调用
Complex * pc = new Complex; //默认构造函数被调用
class Complex {
private :
double real, imag; //代表复数,他有实部和虚部两个变量
public:
Complex( double r, double i = 0);
};
Complex::Complex( double r, double i) { //这是构造函数,对实部和虚部进行初始化
real = r; imag = i;
}
Complex c1; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK,第二个参数可以缺省掉
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);
可以有多个构造函数,参数个数或类型不同
class Complex {
private :
double real, imag;
public:
void Set( double r, double i );
Complex(double r, double i );
Complex (double r );
Complex (Complex c1, Complex c2);
};
Complex::Complex(double r, double i)
{
real = r; imag = i;
}
Complex::Complex(double r)
{
real = r; imag = 0;
}
Complex::Complex(Complex c1, Complex c2);
{
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
Complex c1(3) , c2 (1,0), c3(c1,c2);//c1的整型是可以自动被转换为double 类型的,c2的两个参数同理
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};
构造函数最好是public的,private构造函数不能直接用来初始化对象
class CSample{
private:
CSample() {
}
};
int main(){
CSample Obj; //err. 唯一构造函数是private
return 0;
}
构造函数在数组中的使用:
class CSample {
int x;
public:
CSample() {
cout << "Constructor 1 Called" << endl;
}
CSample(int n) {
x = n;
cout << "Constructor 2 Called" << endl;
}
};
int main(){
CSample array1[2];
cout << "step1"<<endl;
CSample array2[2] = {4,5};
cout << "step2"<<endl;
CSample array3[2] = {3};
cout << "step3"<<endl;
CSample * array4 = new CSample[2];
delete []array4;//在程序结束前要收回
return 0;
}
输出:
因为第一个数组没有给参数,所以数组里的两个元素都是无参的形式。
第二个数组里两个元素都给定了参数,所以都是使用有参的构造函数。
第三个数组只给定了一个参数,所以调用一次有参调用一次无参。
class Test {
public:
Test( int n) { } //(1)
Test( int n, int m) { } //(2)
Test() { } //(3)
};
Test array1[3] = { 1, Test(1,2) };
// 三个元素分别用(1),(2),(3)初始化
Test array2[3] = { Test(2,3), Test(1,2) , 1};
// 三个元素分别用(2),(2),(1)初始化
Test * pArray[3] = { new Test(4), new Test(1,2) };
//两个元素分别用(1),(2) 初始化,指针不初始化也没关系,只生成两个对象
3.复制构造函数
基本概念:
只有一个参数,即对同类对象的引用。
形如 X::X( X& )或X::X(const X &), 二者选一
后者能以常量对象作为参数
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
class Complex {
private :
double real,imag;
};
Complex c1; //调用缺省无参构造函数
Complex c2(c1);//调用缺省的复制构造函数,将 c2 初始化成和c1一样
c2就是c1的复制品,用缺省的复制构造函数创建的。
复制构造函数也可以自己编写,例如:
class Complex {
public :
double real,imag;
Complex(){ }
Complex( const Complex & c ) {
real = c.real;
imag = c.imag;
cout << "Copy Constructor called";//不仅仅复制,还多输出了一句
}
};
Complex c1;
Complex c2(c1);
如果定义的自己的复制构造函数 ,这默认的复制构造函数不存在。
不允许有形如 X::X( X )的构造函数。
class CSample {
CSample( CSample c ) {
} //错,不允许这样的构造函数
};
复制构造函数起作用的三种情况:
- 当用一个对象去初始化同类的另一个对象时。
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句
- 如果某函数有一个参数是类 A 的对象,那么该函数被调用时,类A的复制构造函数将被调用。
class A
{
public:
A() { };
A( A & a) {
cout << "Copy constructor called" <<endl;
}
};
void Func(A a1){ }
int main(){
A a2;
Func(a2);
return 0;
}
- 如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用:
class A
{
public:
int v;
A(int n) { v = n; };
A( const A & a) {
v = a.v;
cout << "Copy constructor called" <<endl;
}
};
A Func() {
A b(4);
return b;
}
int main() {
cout << Func().v << endl; return 0;
}
输出结果:
Copy constructor called
4
注意:对象间赋值并不导致复制构造函数被调用
class CMyclass {
public:
int n;
CMyclass() {};
CMyclass( CMyclass & c) { n = 2 * c.n ; }
};
int main() {
CMyclass c1,c2;
c1.n = 5; c2 = c1; CMyclass c3(c1);
cout <<"c2.n=" << c2.n << ",";
cout <<"c3.n=" << c3.n << endl;
return 0;
}
输出: c2.n=5,c3.n=10
常量引用参数的使用:
void fun(CMyclass obj_ ) {
cout << "fun" << endl;
}
这样的函数,调用时生成形参会引发复制构造函数调用,开销比较大。
所以可以考虑使用 CMyclass & 引用类型作为参数。
如果希望确保实参的值在函数中不应被改变,那么可以加上const 关键字:
void fun(const CMyclass & obj) {
//函数中任何试图改变 obj值的语句都将是变成非法
}
为什么自己要写复制构造函数?
将来再说。
4. 类型转换构造函数和析构函数
什么是类型转换构造函数?
定义转换构造函数的目的是实现类型的自动转换。
只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。
当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
实例:
class Complex {
public:
double real, imag;
Complex( int i) {//类型转换构造函数
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r,double i) {real = r; imag = i; }
};
int main ()
{
Complex c1(7,8);
Complex c2 = 12;
c1 = 9; // 9被自动转换成一个临时Complex对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}
class Complex {
public:
double real, imag;
explicit Complex( int i) {//显式类型转换构造函数
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r,double i) {real = r; imag = i; }
};
int main () {
Complex c1(7,8);
Complex c2 = Complex(12);
c1 = 9; // error, 9不能被自动转换成一个临时Complex对象
c1 = Complex(9) //ok
cout << c1.real << "," << c1.imag << endl;
return 0;
}
什么是析构函数?
名字与类名相同,在前面加‘~’, 没有参数和返回值,一个类最多只能有一个析构函数。
析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。
如果定义了析构函数,则编译器不生成缺省析构函数。
析构函数实例:
class String{
private :
char * p;
public:
String () {
p = new char[10];
}
~ String () ;
};
String ::~ String()
{
delete [] p;
}
对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。
class Ctest {
public:
~Ctest() { cout<< "destructor called" << endl; }
};
int main () {
Ctest array[2];
cout << "End Main" << endl;
return 0;
}
输出:
End Main
destructor called
destructor called
delete 运算导致析构函数调用。
Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest; //析构函数调用
---------------------------------------------------------
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次
若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)
class CMyclass {
public:
~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj ) { //参数对象消亡也会导致析
//构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main(){
obj = fun(obj); //函数调用的返回值(临时对象)被
return 0; //用过后,该临时对象析构函数被调用
}
输出:
destructor
destructor
destructor
5. 构造函数析构函数调用时机
#include<bits/stdc++.h>
#include<iostream>
#include<string>
using namespace std;
class Demo {
int id;
public:
Demo(int i) {
id = i;
cout << "id=" << id << " constructed" << endl;
}
~Demo() {
cout << "id=" << id << " destructed" << endl;
}
};
Demo d1(1); //第一次调用构造函数。输出id =1 constructed
void Func()
{
static Demo d2(2);
Demo d3(3);
cout << "func" << endl;
}
int main () {
Demo d4(4); //局部对象,它被调用引发第二次调用构造函数,输出id = 4 constructed
d4 = 6; //因为有类型转换构造函数,因此 6 会转换成临时对象,会调用构造和析构,
//所以执行id = 6 constructed。临时对象消亡时,又会执行id =6 destructed
cout << "main" << endl; //这里输出一个 main
{ Demo d5(5); //局部变量他的生存期是包含它的大括号以内。这里调用构造函数,输出id = 5 constaructed
} //所以在这 d5 消亡。输出 id = 5 destructed
Func(); //调用Func 创建静态对象d2 对象d3 ,输出 id =2 constructed,id =3 constructed .输出 func
//Func调用结束,d2因为是静态对象,所以不会消亡(静态需看概念),程序结束静态对象才会消亡。但是 d3 (3)会消亡,输出id = 3 destructed 。
cout << "main ends" << endl; //输出main ends,这里main结束,局部变量d4消亡,因为前面
//d4的值变成6,所以输出 id = 6 destructed。
return 0; //整个函数结束,全局对象d1还有静态对象d2消亡,输出
} //id = 1 destructed。id = 2 destructed。
执行结果:
构造函数不管房子的装修,只负责初始化,内存已经在之前就分配完了,也就是只负责装修。
而析构函数不负责拆迁,只不过在拆掉之前,进到房子里做一些善后,就是把值钱的东西都搬走。
例题1:
假设A是一个类的名字,下面的程序片段会类A的调用析构函数几次?
int main() {
A * p = new A[2];
A * p2 = new A;
A a;
delete [] p;
}
答:3次
因为*p2没有delete所以不会消亡,哪怕main结束都不会。
不同编译器 复制构造函数表现不同(了解)
学习时间:
1、 2021.3.19周五11点—下午17.00
2、 周六上午 9 点-上午 11 点
3、 周日下午 3 点-下午 6 点
学习产出:
无