程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础

笔记按照中国大学MOOC上北京大学郭炜老师主讲的程序设计与算法(三)C++面向对象程序设计所作,B站上也有资源。原课程链接如下:

程序设计与算法(三)C++面向对象程序设计

其他各章节链接如下:

程序设计与算法(三)C++面向对象程序设计笔记 第一周 从C到C++

程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础

程序设计与算法(三)C++面向对象程序设计笔记 第三周 类和对象提高

程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载

程序设计与算法(三)C++面向对象程序设计笔记 第五周 继承

程序设计与算法(三)C++面向对象程序设计笔记 第六周 多态

程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板

程序设计与算法(三)C++面向对象程序设计笔记 第八周 标准模板库STL(一)

程序设计与算法(三)C++面向对象程序设计笔记 第九周 标准模板库STL(二)

程序设计与算法(三)C++面向对象程序设计笔记 第十周 C++11新特性和C++高级主题

其他各科笔记汇总

类和对象基础

结构化程序设计

结构化程序设计

C语言使用结构化程序设计:程序 = 数据结构 + 算法

  • 程序由全局变量以及众多相互调用的函数组成

  • 算法以函数的形式实现,用于对数据结构进行操作

结构化程序设计的程序模式

程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础_第1张图片

结构化程序设计的不足

结构化程序设计中,函数和其所操作的数据结构,没有直观的联系

随着程序规模的增加,程序逐渐难以理解,很难一下子看出:

  • 某个数据结构到底有哪些函数可以对它进行操作?
  • 某个函数到底是用来操作哪些数据结构的?
  • 任何两个函数之间存在怎样的调用关系?

结构化程序设计没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充

难以查错,当某个数据结构的值不正确时,难以找出到底是那个函数导致的

重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用

在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难

总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充(增加新功能),难以查错,难以重用

软件业的目标是更快、更正确、更经济地建立软件

  • 如何更高效地实现函数的复用?
  • 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护

面向对象程序设计

面向对象的程序设计

面向对象的程序设计方法,能够较好解决上述问题

面向对象的程序 = 类 + 类 + …+ 类

 

设计程序的过程,就是设计类的过程

面向对象的程序设计方法:

  • 将某类客观事物共同特点(属性)归纳出来,形成一个数据结构(可以用多个变量描述事物的属性)
  • 将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)

然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装

面向对象的程序设计具有**“抽象”,“封装”,“继承”,“多态”**四个基本特点

面向对象的程序模式

程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础_第2张图片

类和对象

从客观事物抽象出类

写一个程序,输入矩形的长和宽,输出面积和周长

比如对于“矩形”这种东西,要用一个类来表示,该如何做“抽象”呢?

矩形的属性就是长和宽。因此需要两个变量,分别代表长和宽

一个矩形,可以有哪些行为呢(或可以对矩形进行哪些操作)?

  • 矩形可以有设置长和宽,算面积,和算周长这三种行为(当然也可以有其他行为)
  • 这三种行为,可以各用一个函数来实现,他们都需要用到长和宽这两个变量

将长、宽变量和设置长,宽,求面积,以及求周长的三个函数“封装”在一起,就能形成一个“矩形类”

长、宽变量成为该“矩形类”的“成员变量”,三个函数成为该类的“成员函数” 。 成员变量和成员函数统称为类的成员

实际上,“类”看上去就像“带函数的结构”

class CRectangle
{
    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();
    return 0;
}

通过类,可以定义变量。类定义出来的变量,也称为类的实例,就是我们所说的“对象

C++中,**类的名字就是用户自定义的类型的名字。可以像使用基本类型那样来使用它。**CRectangle 就是一种用户自定义的类型

 

类的成员函数和类的对象可以分开写

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 类的成员函数,而非普通函数。那么一定要通过对象或对象的指针或对象的引用才能调用

对象的内存分配

和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和

对于上面的CRectangle类,sizeof(CRectangle) =8

成员函数会占用存储空间,但一个类的成员函数在内存里只有一份,被所有对象所共享,它不会被放到对象里面

每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象

对象间的运算

和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”

使用类的成员变量和成员函数

用法1:对象名.成员名

CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4);

Init 函数作用在 r2 上,即 Init 函数执行期间访问的 w 和 h 是属于 r2 这个对象的, 执行 r2.Init 不会影响到 r1

用法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);

类成员的可访问范围

类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:

  • private: 私有成员,只能在成员函数内访问
  • public : 公有成员,可以在任何地方访问
  • protected: 保护成员,以后再说

以上三种关键字出现的次数和先后次序都没有限制

 

定义一个类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfC4UGas-1666236910178)(C++ 面向对象程序设计.assets/image-20221010185956757.png)]

 

如某个成员前面没有上述关键字,则缺省的被认为是私有成员

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;
}

 

设置私有成员的机制,叫“隐藏

“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改

“隐藏”的作用

如果将上面的程序移植到内存空间紧张的手持设备上,希望将 szName 改为 char szName[5],若szName不是私有,那么就要找出所有类似

strcpy(e.szName,"Tom1234567889");

这样的语句进行修改,以防止数组越界。这样做很麻烦

 

如果将 szName 变为私有,那么程序中就不可能出现(除非在类的内部)

strcpy(e.szName,"Tom1234567889");

这样的语句,所有对 szName 的访问都是通过成员函数来进行,比如:

e.setName( “Tom12345678909887”);

那么,就算 szName 改短了,上面的语句也不需要找出来修改,只要改 setName 成员函数,在里面确保不越界就可以了

成员函数的重载及参数缺省

成员函数的重载及参数缺省

成员函数也可以重载

成员函数可以带缺省参数

#include 
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();  //输出5
    return 0;
}
使用缺省参数要注意避免有函数重载时的二义性
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

构造函数(constructor)

基本概念

成员函数的一种

  • 名字与类名相同,可以有参数,不能有返回值(void也不行)
  • 作用是对对象进行初始化,如给成员变量赋初值。对象占用的存储空间不是构造函数分配的
  • 如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数,默认构造函数无参数,不做任何操作
  • 如果定义了构造函数,则编译器不生成默认的无参数的构造函数
  • 对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
  • 一个类可以有多个构造函数

 

为什么需要构造函数:

  1. 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数

  2. 有时对象没被初始化就使用,会导致程序出错

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,整型可以自动被转换成 double 类型
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 = {3, 0}, c2 = {1, 0}, c3 = {4, 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;
}

输出:
Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called

 

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) 初始化

pArray 是指针数组而不是对象数组,定义 pArray 数组不会生成对象,不会调用 Test 构造函数

复制构造函数(copy constructor)

基本概念

只有一个参数,即对同类对象的引用

形如 X::X( X& )X::X(const X &) , 二者选一,后者能以常量对象作为参数

如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能

class Complex {
         private:
                  double real,imag;
};
Complex c1;    //调用缺省无参构造函数
Complex 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);//调用自己定义的复制构造函数,输出 Copy Constructor called

 

不允许有形如 X::X( X ) 的构造函数

class CSample {
        CSample( CSample c ) {
        } //错,不允许这样的构造函数
};
复制构造函数起作用的三种情况

1)当用一个对象去初始化同类的另一个对象时

Complex c2(c1);

Complex c2 = c1; //初始化语句,非赋值语句

2)如果某函数有一个参数是类 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;
}

程序输出结果为: Copy constructor called

3)如果函数的返回值是类 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 值的语句都将是变成非法
}

类型转换构造函数

什么是类型转换构造函数

定义转换构造函数的目的是实现类型的自动转换

只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数

当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)

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;
}

析构函数(destructors)

基本概念

名字与类名相同,在前面加 “~”, 没有参数和返回值,一个类最多只能有一个析构函数

析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等

如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做

如果定义了析构函数,则编译器不生成缺省析构函数

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

 

main 结束时,局部变量 array 会消亡

析构函数和运算符 delete

delete 运算导致析构函数调用

Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest;         //析构函数调用

new 出来的一定要 delete 才能消亡,哪怕整个程序结束了也不会消亡

 

若 new 一个对象数组,那么用 delete 释放时应该写 []。否则只 delete 一个对象(调用一次析构函数)

pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest;      //析构函数调用3次
析构函数在对象作为函数返回值返回后被调用
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

 

临时对象一般都是在包含临时对象的这条语句执行完后消亡

第一个 destructor —— 函数的形参对象消亡时引发析构函数调用输出

第二个 destructor —— 临时对象消亡时引起

第三个 destructor —— 整个程序结束时全局对象消亡引发析构函数输出

构造函数和析构函数什么时候被调用?

构造函数和析构函数什么时候被调用?
class Demo {
       int id;
       public:
              Demo(int i) {
                    id = i;
                    cout << "id=" << id << " constructed" << endl;
              }
              ~Demo() {
                    cout << "id=" << id << " destructed" << endl;
              }
};


Demo d1(1);
void Func()
{
       static Demo d2(2);
       Demo d3(3);
       cout << "func" << endl;
}
int main () {
       Demo d4(4);
       d4 = 6;
       cout << "main" << endl;
       { 
           Demo d5(5);
       }
       Func();
       cout << "main ends" << endl;
       return 0;
}

输出结果: :
id=1 constructed
id=4 constructed
id=6 constructed
id=6 destructed
main
id=5 constructed
id=5 destructed
id=2 constructed
id=3 constructed
func
id=3 destructed
main ends
id=6 destructed
id=2 destructed
id=1 destructed

 

全局对象 d1 在 main 执行之前就被初始化

类型转换构造函数 Demo 可以自动把整型变量 6 转换为临时对象,临时对象执行完赋值语句后就消亡

C 语言中局部变量的生存期从程序走到定义它的语句开始,到包含它的最内层那一对大括号的右边为止

静态局部变量 d2 在函数结束时不消亡,直到整个程序结束静态局部对象才消亡

main 结束时引发局部对象 d4 消亡

整个程序结束,全局对象 d1 和静态局部对象 d2 消亡,一般先初始化构造的后析构

复制构造函数在不同编译器中的表现
class A {
    public:
        int x;
        A(int x_):x(x_)   //构造函数
           { cout << x << " constructor called" << endl; }  
        A(const A & a ) { // 本例中dev需要此const其他编译器不要
               x = 2 + a.x;
               cout << "copy called" << endl;
        }
        ~A() { cout << x << " destructor called" << endl; }
};

A f( ){ A b(10); return b; }

int main( ){
    A a(1);
    a = f();
    return 0;
}

Visual Studio输出结果:
1 constructor called
10 constructor called
copy called
10 destructor called
12 destructor called
12 destructor called

dev C++输出结果:
1 constructor called
10 constructor called
10 destructor called
10 destructor called

 

说明dev出于优化目的并未生成返回值临时对象。VS无此问题

你可能感兴趣的:(C/C++学习,算法,c++,数据结构,笔记,学习)