of {$slidecount} ½ {$title} ATZJG.NET {$author}

首页






引论
— C++ 类 —


Haifeng Xu


(hfxu@yzu.edu.cn)

This slide is based on Mark Allen Weiss's book.
中译本是《数据结构与算法分析 C++ 描述》张怀勇等译

目录

关于数据结构

关于数据结构

数据结构是计算机专业的一门重要的基础课, 在计算机科学各领域尤其是在软件设计和开发中发挥着举足轻重的作用.

几乎所有的计算机软件系统, 例如, 操作系统、编辑工具和编译器等都要使用不同的数据结构.

因此, 数据结构是计算机专业的核心课程, 是许多其他后续课程的重要基础.

何为数据结构

数据结构(data structure)指的是存储数据的结构.

我们可以将数据结构看作工具箱中的一套工具.

参考文献

参考文献

本课程使用C++语言来描述数据结构和算法分析, 因此假设读者具有 C++ 的基础知识. 这方面的参考文献有

该书由 C++ 的创始人 Bjame Stroustrup 所编写, 描述了 C++ 的最新设计标准, 具有最高的权威性.

另一本标准的参考文献是

这两本书都有中译本. 另外钱能写的《C++程序设计教程》(第二版)也不错.

关于C++的高级编程可以参考

另外 S. Meyers 写的以下两本书详尽地讨论了 C++ 的许多缺陷.

关于数据结构的参考书

1.4 C++ 类

C++ 类

类(class)指的是一段程序代码, 其中封装了一些数据和一些方法(也称函数). 由类可以生成很多对象. (可以将类中数据的组织方式连同方法一起称为数据结构).

面向对象编程(object-oriented programming, OOP) 的基础就是由类生成一个或多个对象这种编程方式.

结构(struct)是一个类似于类的结构, 但是它通常要比类简单些. 其仅封装数据, 而不包含方法.

结构

结构与数组类似, 因为结构也可以用来存储数据的多个元素, 但是, 在数组中所有元素的类型都必须是相同的类型.

#include <iostream>
#include <iomanip>
#include <string>

using namespace std;

// 结构名通常使用大写
struct CarType {
	string maker;// 结构的元素通常称为数据成员(data member)或简称为成员.
	int year;
	float price;
};//别忘了分号

void getYourCar (CarType & car);

int main()
{
	// CarType 是一个自定义类型
	CarType myCar, yourCar;// 变量 myCar, yourCar 使用结构类型声明时, 就生成了对象(object)
	//使用同一个结构可以生成一个或多个对象.
	
	
	myCar.maker = "Mercedes"; // I wish
	myCar.year = 2005;// 结构成员的访问, 使用 . 操作符
	myCar.price = 45567.75;
	
	getYourCar (yourCar);
	
	cout << "Your car is a: " << yourCar.maker << endl;
	cout << fixed << showpoint << setprecision(2) <<
		 "I'll offer $" << yourCar.price -100 << " for your car."
		 << endl;
		 
	return 0;
}

void getYourCar (CarType & car)
{
	cout << "Enter your maker: ";
	cin >> car.maker;
	cout << "Enter the year: ";
	cin >> car.year;
	cout << "Enter the price: $";
	cin >> car.price;
}

对象数组

CarType dealerCar[100];

结构中声明的成员可以是数组, 也可以是其他结构.

struct EngineType {
	int numCylinders; // 气缸数
	float numLiters;  // 升
	string countryMade;
};

struct CarType {
	string maker;
	float price;
	string wheel[4];
	EngineType engine; // 前面一定要先定义 EngineType
};

赋值

myCar.engine.numCylinders = 6;
myCar.wheel[2] = "Goodyear";

结构的对象可以赋值给同一结构的另一个对象.

yourCar = myCar;

这个代码将 myCar 的每个成员的值赋给 yourCar 中相应的成员.


C# code

using System;	// 通用基础类库类型
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace getCarType
{
    public struct CarType
    {
        public string maker;
        public int year;
        public float price;

        public void getYourCar()
        {
            Console.Write("Enter your maker: ");
            maker = Console.ReadLine();

            Console.Write("Enter the year: ");
            if (int.TryParse(Console.ReadLine(), out year))
            {

            }
            else {
                year = 2000;
            }
            
            Console.Write("Enter the price: $");
            if (float.TryParse(Console.ReadLine(), out price))
            {

            }
            else {
                price = 0F;
            }
        }

        public void printTheCar()
        {
            Console.WriteLine("The info of the car is:");
            Console.WriteLine("Maker:" + maker);
            Console.WriteLine("Year:" + year);
            Console.WriteLine("Price:" + price);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            CarType myCar;
            CarType yourCar;

            myCar.maker = "Mercedes";
            myCar.year = 2005;
            myCar.price = 45567.75F;

            myCar.printTheCar();

            yourCar = myCar;

            yourCar.getYourCar();
            yourCar.printTheCar();

            Console.WriteLine("Press any key to terminate this program.");
            Console.ReadKey();

        }


        
    }
}


头文件 iomanip

iomanip.h 是I/O流控制头文件, 就像C里面的格式化输出一样. 在新版本的c++中头文件已经用iomanip取代了iomanip.h.

以下是一些常用的函数:

Reference:
http://www.cppblog.com/masiyou/archive/2009/10/06/97948.aspx

基本的类语法

基本的类语法

类和结构的区别

对象内部的数据只能由对象本身访问, 我们称这一特性为封装(encapsulation).

例子 1.5 IntCell 类

#include <iostream>
using namespace std;

/**
 * A class for simulating an integer memory cell.
 */
class IntCell
{
  public:
    /**
     * Construct the IntCell.
     * Initial value is 0.
     */
    IntCell( )
      { storedValue = 0; }

    /**
     * Construct the IntCell.
     * Initial value is initialValue.
     */
    IntCell( int initialValue )
      { storedValue = initialValue; }

    /**
     * Return the stored value.
     */
    int read( )
      { return storedValue; }

    /**
     * Change the stored value to x.
     */
    void write( int x )
      { storedValue = x; }

  private:
    int storedValue;
};

int main( )
{
    IntCell m;

    m.write( 5 );
    cout << "Cell contents: " << m.read( ) << endl;

    return 0;
}


C# code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IntCell
{
    class IntCell
    {
        // member variables
        int storedValue;

        public IntCell()
        {
            storedValue = 0;
        }

        IntCell(int initialValue)
        {
            storedValue = initialValue;
        }

        public int read()
        {
            return storedValue;
        }

        public void write(int x)
        {
            storedValue = x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //IntCell m;//声明, 但未实例化.  C#是有保护机制,在使用变量的时候必须赋值。
            IntCell m = new IntCell();

            m.write(5);
            Console.WriteLine("Cell contents: " + m.read());
            Console.WriteLine("\nPress any key to terminate this program.");
            Console.ReadKey();
        }
    }
}


Java code

//File: MemoryCell.java
/*
// MemoryCell class
// Object read()  -->  Returns the stored value
// void write(Object x)  -->  x is stored
*/
public class MemoryCell
{
  //public methods
  public Object read()
  {
    return storedValue;
  }
  
  public void write(Object x)
  {
    storedValue=x;
  }

  //Private internal data representation
  private Object storedValue;
}


//TestMemoryCell.java
/*
//Will use the class MemoryCell
*/
public class TestMemoryCell
{
  public static void main(String[] args)
  {
    MemoryCell m = new MemoryCell();

	m.write("37");
	String val = (String) m.read();
	System.out.println("Contents are: "+val);
  }
}

信息隐藏

信息隐藏(information hiding)



设计类时应该遵循的原则

C++ 中 public, protected, private 的区别

C++ 中 public, protected, private 的区别

private, public, protected 访问标号的访问范围

注:友元函数包括3种:

类的继承后方法属性变化

Reference:
http://www.cnblogs.com/york-hust/archive/2012/06/01/2530799.html

构造函数

构造函数(constructors)

构造函数是描述如何构建类的实例(即对象)的方法.

特殊的构造函数语法与访问函数

例 1.6, 改进的 IntCell 类


#include <iostream>
using namespace std;

/**
 * A class for simulating an integer memory cell.
 */
class IntCell
{
  public:
    explicit IntCell( int initialValue = 0 )
      : storedValue( initialValue ) { } //初始化列表
    int read( ) const         //加 const 表示 read() 是访问函数
      { return storedValue; }
    void write( int x )
      { storedValue = x; }
  private:
    int storedValue;
};

int main( )
{
    IntCell m;

    m.write( 5 );
    cout << "Cell contents: " << m.read( ) << endl;

    return 0;
}

默认参数(default parameter)

这里仍然定义了两个名为 IntCell 的构造函数

默认参数可以在任何函数中使用, 但是最普遍的情况是用在构造函数中.


初始化列表(initializer list)

构造函数在其函数体之前使用初始化列表. 初始化列表用来直接初始化数据成员, 用冒号 : 开始, 用逗号 , 间隔.

在数据成员是具有复杂初始化过程的类类型的时候, 使用初始化列表代替代码体中的赋值语句可以节省很多时间. 在某些情况下, 这是很有必要的.


explict 构造函数

IntCell obj;	//obj is an IntCell
obj = 37;	//Should not compile: type mismatch
obj.write(37);	//work

如果未指定 explicit, 则通常单参数构造函数定义了一个隐式类型转换(implicit type conversion), 该转换创建了一个临时对象, 从而使下面的赋值变成兼容.

obj = 37;	//编译器试图将此转换为
IntCell temporary = 37;
obj =  temporary;

常成员函数(constant member function)

在 C++ 中, 每个成员函数都被标记为访问函数或修改函数. 在设计阶段这是很重要的一步, 不可以被简单地看成注释.

接口与实现分离

接口与实现分离

C++ 程序通常将类的说明和实现分别放在两个文件中. 所谓接口就是指类的说明.

# 称为预处理器指示符(preprocessor include directive). 处理 这些指示符的程序被称做预处理器(preprocessor), 通常捆绑在编译器中.

IntCell.h

/**
 * 在一个复杂的项目中, 往往包含很多文件. 有的文件包含其他文件.
 * 下面的预处理命令避免了文件内容被重复读取的危险.
 * 一般定义的符号与文件名基本相一致.
 * 条件指示符 #ifndef 检查 IntCell_H 在前面是否已经被定义
 */
#ifndef IntCell_H
#define IntCell_H

/**
 * A class for simulating an integer memory cell.
 */
class IntCell
{
  public:
    explicit IntCell( int initialValue = 0 );
    int read( ) const;
    void write( int x );
  private:
    int storedValue;
};

#endif

IntCell.cpp

#include "IntCell.h"

/**
 * Construct the IntCell with initialValue
 * 成员函数必须声明为类的一部分, 否则函数将会被认为是全局的.
 * 语法是 ClassName::member
 * :: 称为是作用域运算符.
 */
IntCell::IntCell( int initialValue ) : storedValue( initialValue )
{
}

/**
 * Return the stored value.
 */
int IntCell::read( ) const
{
    return storedValue;
}

/**
 * Store x.
 */
void IntCell::write( int x )
{
    storedValue = x;
}

TestIntCell.cpp

#include <iostream>
#include "IntCell.h"
using namespace std;

int main( )
{
    IntCell m;   // Or, IntCell m( 0 ); but not IntCell m( );

    m.write( 5 );
    cout << "Cell contents: " << m.read( ) << endl;

    return 0;
}


其他要注意的是:

vector 和 string

vector 和 string

C++ 标准定义了两个类 vector 和 string. vector 意在替代带来无穷麻烦的 C++ 内置数组.

内置数组的缺点

例子 1.10

/**
 * 创建一个存储100个平方值的向量, 并且将这些数据输出.
 */
#include <iostream>
#include <vector>
using namespace std;

int main( )
{
    vector<int> squares( 100 );//这里squares向量中每个元素初始化为0

    for( int i = 0; i < squares.size( ); i++ )
        squares[ i ] = i * i;

    for( int i = 0; i < squares.size( ); i++ )
        cout << i << " " << squares[ i ] << endl;

    return 0;
}

1.5 C++ 细节

C++ 细节

指针

指针(Pointers)

指针变量(pointer variable) 是用来存储其他对象之存储地址的变量.

/**
 * 这里仅是为简要说明动态分配内存的, 一般对于简单的 IntCell 类, 
 * 无需这样写代码. 但当遇到复杂的类时, 会看到动态分配内存这种技术很有用且很必要.
 */
#include <iostream>
#include "IntCell.h"
using namespace std;

int main( )
{
    IntCell *m; // 声明 m 是一个指针变量, 指向 IntCell 类对象. m 在这里未进行初始化

    m = new IntCell( 0 );// 假设这里抛出异常, 则根本不会执行到下面的 delete m; 语句
    m->write( 5 );
    cout << "Cell contents: " << m->read( ) << endl;

    delete m;// 不就地处理异常会很危险
    return 0;
}

动态对象的创建

在 C++ 中有两种方式可以使用零参数构造函数来创建对象.

m = new IntCell();	// OK, 但有可能被误认为是函数的声明.
m = new IntCell;	// OK, 最好采用这种.

垃圾收集及 delete

在一些语言中, 当一个对象不再引用时, 就纳入自动垃圾收集的范围. 如 Java

C++ 没有垃圾收集. 当一个通过 new 来分配地址的对象不再被引用时, 就需要使用 delete 操作(通过指针)将其删除.

否则, 该对象所占的内存就不能被释放(直到程序结束). 这被称为内存泄露(memory leak).

指针的赋值和比较

通过指针访问对象的成员

如果指针变量指向类类型的对象, 那么该对象的(可见)成员就可以使用 -> 操作符进行访问.

其他指针运算

C++ 允许对指针进行各种特殊的运算, 这些运算偶尔是很有用的. 如: 比较运算符 < 和取地址运算符 &.

参数传递

参数传递

C++ 有三种不同的方式来传递参数.

/** 下面定义的函数 avg 返回数组 arr 中前 n 个整数的平均值.
 *  并且如果 n 大于 arr.size() 或者小于 1, 就设定 errorFlag 为 true.
 */

double avg( const vector<int> & arr, int n, bool & errorFlag);
  1. arr 是 vector<int> 类型的, 使用按常量引用调用(call by constant reference)来传递;
  2. n 是 int 类型的, 通过按值调用(call by value)来传递;
  3. errorFlag 是 bool 类型的, 使用引址调用(call by reference)来传递.

参数传递机制的选用规则

返回值的传递

返回值的传递

对象的返回也可以是按值返回和按常量引用返回, 偶尔也用到引址返回.

例子 1.12

/** OK
 * arr[maxIndex] 索引的 vector 是在 findMax 外部的, 并且存在时间长于调用返回的时间.
 */
const string & findMax( const vector<string> & arr )
{
    int maxIndex = 0;

    for( int i = 1; i < arr.size( ); i++ )//这里应将 int 改为 unsigned int, 否则编译器警告: 在无符号数与有符号数之间比较
        if( arr[ maxIndex ] < arr[ i ] )
            maxIndex = i;

    return arr[ maxIndex ];
}

/** WRONG
 * maxValue 是一个局部变量, 当函数返回时就不复存在了.
 * Linux 下编译得到警告:返回了对局部变量的‘maxValue’的引用
 */
const string & findMaxWrong( const vector<string> & arr )
{
    string maxValue = arr[ 0 ];

    for( int i = 1; i < arr.size( ); i++ )
        if( maxValue < arr[ i ] )
            maxValue = arr[ i ];

    return maxValue;
}

引用变量

引用变量(reference variables)

引用变量和常量引用变量(const reference variables)通常用于参数传递. 但它们也可以用作局部变量或者类的数据成员.

在这些情况下, 变量名就是它所引用的对象名的同义词.

作为局部变量, 它们避免了复制的成本, 因此在对含有类类型集合的数据结构进行排序时非常有用.

三大函数: 析构函数、复制构造函数和 operator=

三大函数: 析构函数、复制构造函数和 operator=

在 C++ 中, 伴随类的是已经写好的三个特殊函数, 它们是析构函数(destructor)、复制构造函数(copy constructor)和 operator=.

析构函数(Destructor)

当一个对象超出其作用域或执行 delete 时, 就调用析构函数.

通常, 析构函数的唯一任务就是释放使用对象时所占有的所有资源. 这其中包括

复制构造函数(copy constructor)

有一种特殊的构造函数, 用于构造新的对象, 被初始化为相同类型对象的一个副本, 这就是复制构造函数(copy constructor).

以 IntCell 对象为例, 复制构造函数可以如下进行调用:

operator=

operator=

当 = 应用于两个已经构造的对象时, 就调用复制赋值运算符 operator=.

默认值带来的问题

默认值带来的问题

主要的问题出现在类数据成员是指针的情形.


当一个类含有的数据成员为指针并且深复制很重要时, 一般的做法就是必须实现析构函数、 operator= 和复制构造函数.

对于 IntCell, 这些运算的签名是

~IntCell();	//destructor
IntCell(const IntCell & rhs);	//copy constructor
const IntCell & operator=(const IntCell & rhs);	//operator=
/**
 * Fig 1.13  三大函数的默认值
 */

IntCell::~IntCell( )
{
    // Does nothing, since IntCell contains only an int data
    // member. If IntCell contained any class objects, their
    // destructors would be called.
}

//可以使用 IntCell B(C);
IntCell::IntCell( const IntCell & rhs ) : storedValue( rhs.storedValue )
{
}

//operator= 是我们最感兴趣的. 
//可以使用 IntCell B = C;
const IntCell & IntCell::operator=( const IntCell & rhs )
{
    if( this != &rhs )   // Standard alias test, (别名测试, 以确保没有复制自身)
        storedValue = rhs.storedValue;
    return *this; // 返回对当前对象的引用, 于是赋值可以链状进行, 如 a=b=c.
}

当默认值不可用时

当默认值不可用时

最常见的默认值不可用的情况是数据成员是指针类型的, 并且被指对象(pointee)通过某些对象成员函数(例如构造函数)来分配地址.

举例说明, 假设 IntCell 是通过动态分配一个 int 来实现的, 如下面所示

// Fig 1.14 数据成员是指针, 默认值不适用
class IntCell
{
  public:
    explicit IntCell( int initialValue = 0 )
      { storedValue = new int( initialValue ); }

    int read( ) const
      { return *storedValue; }
    void write( int x )
      { *storedValue = x; }
  private:
    int *storedValue;
};

在下面的 Fig 1.15 中暴露了 Fig 1.14 大量的问题.

// Fig 1.15
int f( )
{
    IntCell a( 2 );
    IntCell b = a;
    IntCell c;

    c = b;
    a.write( 4 );
    cout << a.read( ) << endl << b.read( ) << endl << c.read( ) << endl;
    return 0;
}

解决的办法如下图(接口与实现已经分离)

// Fig 1.16
class IntCell
{
  public:
    explicit IntCell( int initialValue = 0 );

    IntCell( const IntCell & rhs );
    ~IntCell( );
    const IntCell & operator=( const IntCell & rhs );

    int read( ) const;
    void write( int x );
  private:
    int *storedValue;
};

IntCell::IntCell( int initialValue )
{
    storedValue = new int( initialValue );
}

IntCell::IntCell( const IntCell & rhs )
{
    storedValue = new int( *rhs.storedValue );// 这里先 rhs.storedValue, 注意 rhs 是对象变量
}

IntCell::~IntCell( )
{
	//默认, 析构函数体内一般不需要再添加代码, 但是 IntCell 中有指针成员, 有 new 操作, 因此必须释放.
    delete storedValue;
}

const IntCell & IntCell::operator=( const IntCell & rhs )
{
    if( this != &rhs )
        *storedValue = *rhs.storedValue;
    return *this;
}

int IntCell::read( ) const
{
    return *storedValue;
}

void IntCell::write( int x )
{
    *storedValue = x;
}

C 风格的数组和字符串

C 风格的数组和字符串

C++ 语言提供内置的 C 风格的数组类型. 如

int arr1[10];

在上面的定义中, 在编译的时候数组的大小必须是已知的. 10 不可以用变量来代替. 如果数组的大小未知, 就必须显式声明一个指针, 并且用 new[] 来分配内存. 例如

int *arr2 = new int[n];

现在 arr2 的作用就和 arr1 一样了, 只不过它不是常量指针.


内置的 C 风格的字符串

内置的 C 风格的字符串是当作字符数组来实现的.

这些字符串有着数组所包含的所有问题, 包括困难的内存管理问题.

标准的 vector 类和 string 类在实现时隐藏了内置的 C 风格的数组和指针的操作.

模板(Templates)

模板(Templates)

本节讨论在 C++ 中如何用模板(template)来写类型无关的算法, 也称为泛型算法(generic algorithms).


函数模板(function template)

函数模板不是真正的函数, 而是一个用以生成函数的模式(pattern).

/**
 * Fig 1.17 findMax 函数模板 (与 Fig 1.12 关于 string 的 findMax 版本几乎完全一致)
 * Return the maximum item in array a.
 * Assumes a.size( ) > 0.
 * Comparable objects must provide operator< and operator=
 */
template <typename Comparable>
const Comparable & findMax( const vector<Comparable> & a )
{
    int maxIndex = 0;

    for( int i = 1; i < a.size( ); i++ )
        if( a[ maxIndex ] < a[ i ] )
            maxIndex = i;
    return a[ maxIndex ];
}

/**
 * Fig 1.18 使用 findMax 函数模板
 */
int main( )
{
    vector<int>     v1( 37 );
    vector<double>  v2( 40 );
    vector<string>  v3( 80 );
    vector<IntCell> v4( 75 );

    // Additional code to fill in the vectors not shown

    cout << findMax( v1 ) << endl;  // OK: Comparable = int
    cout << findMax( v2 ) << endl;  // OK: Comparable = double
    cout << findMax( v3 ) << endl;  // OK: Comparable = string
    cout << findMax( v4 ) << endl;  // Illegal; operator< undefined

    return 0;
}

函数模板可以应需要而自动扩展. 要注意的是随着每种类型的扩展, 都会生成附加代码. 在大项目里, 这被称为 代码膨胀(code bloat).

需要注意的是, 上面调用 findMax(v4) 会导致编译时出错, 这是因为当 IntCell 替换掉 Comparable 后,

if( a[ maxIndex ] < a[ i ] ) // 这一行变成非法的

原因在于没有为类 IntCell 定义 < 函数.

因此, 在使用任何模板之前, 习惯上都是

有很多处理函数模板的晦涩的规则. 大多数的问题都是出现在函数模板不能提供完全匹配的而只是相近的参数(通过隐式类型转换)的情况下. 必须有解决这种不确定问题的办法, 其规则也是非常复杂的.

注意:


上机实验

完成 Fig 1.17, Fig 1.18 的实验, 注意将 IntCell.h, IntCell.cpp 包含进来.

类模板

类模板(Class Templates)

这里给出的例子, 类模板与函数模板的运行情况相似.

/**
 * Fig 1.19 未分离的 MemoryCell 类模板
 * A class for simulating a memory cell.
 */
template <typename Object>
class MemoryCell
{
  public:
    explicit MemoryCell( const Object & initialValue = Object( ) ) // 注意
      : storedValue( initialValue ) { }
    const Object & read( ) const
      { return storedValue; }
    void write( const Object & x )
      { storedValue = x; }
  private:
    Object storedValue;
};

要注意的是,

Fig 1.20 给出了 MemoryCell 怎样存储基本类型和类类型对象的例子.

//Fig 1.20
int main( )
{
    MemoryCell<int>    m1;
    MemoryCell<string> m2( "hello" );

    m1.write( 37 );
    m2.write( m2.read( ) + "world" ); // 这里 + 是指字符串的连接
    cout << m1.read( ) << end1 << m2.read( ) << end1;

    return 0;
}

注意

实验

实验的结果是, 运行速度比较慢, 需要约 1.6 秒. 这也是使用模板的代价.


目前模板的分离编译在许多平台上都不能很好地运行. 因此, 在许多情况下, 包括其实现的整个类都必须放在 .h 文件中. 流行的 STL (Standard Template Library 标准模板库) 的实现就是遵循这个策略.

Object, Comparable 以及一个例子

Object, Comparable 以及一个例子

在本书中, 我们总是将 Object 和 Comparable 作为泛型类型来使用.

例子 Fig 1.21

Fig 1.21 给出了一个实现时需要使用 Comparable 的类类型的例子, 并且例举了 操作符重载(operator overloading).

/**
 * Fig 1.21, Comparable 可以是类类型, 例如这里定义的 Employee 类类型
 * 注意, 如果分离编译, 则声明 string 类时, 最好写为 std::string, 下面的 ostream 也最好改为 std::ostream
 * 否则编译会提示
 * error: ISO C++ forbids declaration of 'string' with no type
 * 如将 Employee 类, findMax 函数模板, main() 函数都放在同一文件中, 则只要在开始时写入 using namespace std; 即可.
 */
class Employee
{
  public:
    void setValue( const std::string & n, double s )
      { name = n; salary = s; }

    const std::string & getName( ) const
      { return name; }
    void print( std::ostream & out ) const
      { out << name << " (" << salary << ")"; }
    bool operator< ( const Employee & rhs ) const
      { return salary < rhs.salary; }

    // Other general accessors and mutators, not shown
    
  private:
    std::string name;
    double salary;
};

// 操作符重载可以允许重新定义内置操作符的含义
// 为新的类类型 Employee 的输出重新定义操作符<<, 其中用到了上面定义的 print 函数.
// Define an output operator for Employee
std::ostream & operator<< ( std::ostream & out, const Employee & rhs )
{
    rhs.print( out );
    return out;
}

int main( )
{
    vector<Employee> v( 3 );

    v[0].setValue( "George Bush", 400000.00 );
    v[1].setValue( "Bill Gates", 2000000000.00 );
    v[2].setValue( "Dr. Phil", 13000000.00 );
    cout << findMax( v ) << endl;

    return 0;
}

Employee 类包含 name 和 salary, 并且在 salary 的基础上定义了 operator<.

Employee 类也提供了零参数构造函数、 operator= 和复制构造函数(没有明确写出来, 都是采用默认值).

这样 Employee 类在 findMax 函数模板中作为 Comparable 类型来使用就足够了.

为具有实用性, 每一个数据成员都必须是 public 的, 否则就必须提供附加的访问函数和修改函数.

Fig 1.21 例举了 setValue 成员函数和为新类类型提供输出函数的广泛使用的惯用例程.

实验

完成 Fig 1.21 的实验.

函数对象

函数对象(Function Objects)

函数模板可以用来实现泛型算法. Fig 1.17 给出了一个用函数模板查找数组中最大项的例子. 但是模板有一个重要的限制:

在许多情况下, 这并不可行. 例如

这些问题的解决方案就是重写 findMax, 使其

在实际效果上, 数组对象不再知道如何进行相互比较, 取而代之的是, 这些信息完全从数组对象中剥离出来.


像传递参数一样传递函数

对象同时包含数据和成员函数, 一个如传递参数一样传递函数的巧妙办法是:

从效果上看, 通过将函数放在对象中实现了函数的传递. 该对象通常称为函数对象(function object).

/** Fig 1.22
 * 函数对象思想的最简单实现
 */
#include <iostream>
#include <vector>
#include <string>
#include <cstring>

using namespace std;

// Generic findMax, with a function object, Version #1.
// Precondition: arr.size( ) > 0.
template <typename Object, typename Comparator>
const Object & findMax( const vector<Object> & arr, Comparator cmp )
{
  unsigned int maxIndex = 0;

   for( unsigned int i = 1; i < arr.size( ); i++ )
        if( cmp.isLessThan( arr[ maxIndex ], arr[ i ] ) )
            maxIndex = i;

   return arr[ maxIndex ];
}

class CaseInsensitiveCompare
{
  public:
    bool isLessThan( const string & lhs, const string & rhs ) const
      { return stricmp( lhs.c_str( ), rhs.c_str( ) ) < 0; }
};

int main( )
{
    vector<string> arr( 3 );
    arr[ 0 ] = "ZEBRA"; arr[ 1 ] = "alligator"; arr[ 2 ] = "crocodile";
    cout << findMax( arr, CaseInsensitiveCompare( ) ) << endl;

    return 0;
}

C++ 的函数对象通过这种基本的思想来实现, 但有一些奇特的语法.

下面的 Fig 1.23 的例程中没有用到函数对象. 其实现是使用标准库函数对象模板 less (在头文件 functional 中定义).

// Fig 1.23, 没有使用函数对象的 findMax 版本
#include <iostream>
#include <vector>
#include <string>
#include <cstring>

using namespace std;

// Generic findMax, with a function object, C++ style.
// Precondition: a.size( ) > 0.
template <typename Object, typename Comparator>
const Object & findMax( const vector<Object> & arr, Comparator isLessThan )
{
    unsigned int maxIndex = 0;

    for( unsigned int i = 1; i < arr.size( ); i++ )
        if( isLessThan( arr[ maxIndex ], arr[ i ] ) )
            maxIndex = i;

    return arr[ maxIndex ];
}

// Generic findMax, using default ordering.
#include <functional>
template <typename Object>
const Object & findMax( const vector<Object> & arr )
{
    return findMax( arr, less<Object>( ) );
}

class CaseInsensitiveCompare
{
  public:
    bool operator( )( const string & lhs, const string & rhs ) const
      { return stricmp( lhs.c_str( ), rhs.c_str( ) ) < 0; }
};

int main( )
{
    vector<string> arr( 3 );
    arr[ 0 ] = "ZEBRA"; arr[ 1 ] = "alligator"; arr[ 2 ] = "crocodile";
    cout << findMax( arr, CaseInsensitiveCompare( ) ) << endl;//本质上调用了 stricmp(), 返回 ZEBRA
    cout << findMax( arr ) << endl;//调用了 std::less::operator(), 返回 crocodile

    return 0;
}

使用矩阵

使用矩阵

C++ 通常不提供标准的 matrix 类, 但可以在网上找到很多个人写的矩阵类. 例如:

我们自己也可以很快写出一个合理的 matrix 类. 基本的思想就是使用向量的向量.

这样做需要附加的操作符重载的知识. 对 matrix 定义 operator[], 即数组索引操作符.

下面给出了一个完整的 matrix 类.

#ifndef MATRIX_H
#define MATRIX_H

#include <vector>
using namespace std;

template <typename Object>
class matrix
{
  public:
    matrix( int rows, int cols ) : array( rows )
    {
        for( int i = 0; i < rows; i++ )
            array[ i ].resize( cols );
    }

//后面会详细解释为什么需要这样两个版本(访问版本和修改版本)的 operator[]
    const vector<Object> & operator[]( int row ) const
      { return array[ row ]; }
    vector<Object> & operator[]( int row )
      { return array[ row ]; }

    int numrows( ) const
      { return array.size( ); }
    int numcols( ) const
      { return numrows( ) ? array[ 0 ].size( ) : 0; }
  private:
    vector< vector<Object> > array;
};

#endif

operator[] 的思想

如果有一个 matrix 对象 m, 那么 m[i] 就应该返回一个对应 m 第 i 行的向量. 这样一来, m[i][j] 就可以通过正常的 vector 索引操作给出向量 m[i] 在位置 j 处的元素. 因此, matrix operator[] 不是返回一个 Object, 而是一个 vector<Object>.

现在我们已经明白 matrix operator[] 应该返回 vector<Object> 的实体. 那么返回应该用值、引址 还是 常量引用呢?

考虑下面的方法(忽略混淆和大小不兼容的可能性, 这两者都不影响算法).

void copy(const matrix<Object> & from, matrix<Object> & to)
{
   for(int i=0;i < to.numrows();i++)
       to[i]=from[i];
}

copy 函数试图复制 matrix from 中的每一行到 matrix to 中相应的行.

注意, 这里参数 from 是常量引用, 而 to 是引用. 因此当在函数体内出现 from[i] 时, 调用的是相应的常量引用的 operator[] 函数; 当出现 to[i] 时, 调用的是普通引用的 operator[] 函数. 因此需要定义两个 operator[] 函数.

分析

假设只定义一个 operator[] 函数.

Matrix 类的析构函数、复制赋值和复制构造函数

因为 vector 已经处理了上述函数, 所以它们在 matrix 类中就不必写了, 都是自动处理的.

初始化

初始化

参考 https://www.cnblogs.com/ArsenalfanInECNU/p/18080526

作业

作业

计算正整数 $n$ 的二进制展开中数字 $1$ 的个数.

int countOnes(unsigned int n)
{
	int ones = 0;
	while (0 < n)
	{
		ones++;
		n&=n-1;
	}
	return ones;
}
  1. $2^{100}(\text{mod}\ 5)$ 是多少?
  2. 设 $\{F_n\}$ 是斐波纳契(Fibonacci)数列, 即 \[F_0=1, F_1=1, F_2=2, F_3=3, F_4=5, \ldots,\] 满足 $F_n=F_{n-1}+F_{n-2}$ $(n\geq 2)$. 证明
    • $F_n<(\frac{5}{3})^n$. 但是 $F_n\leq n^2$ 却不成立, 请编程找出若干反例.
    • \[F_n=\frac{1}{\sqrt{5}}\biggl[(\frac{1+\sqrt{5}}{2})^{n+1}-(\frac{1-\sqrt{5}}{2})^{n+1}\biggr],\quad n\geq 0.\]
  3. 证明 \[\sum_{n=1}^{N}n^k\approx\frac{N^{k+1}}{|k+1|},\quad k\neq -1.\]
  4. 定义函数 $f(x)$ 为 \[f(x)=\begin{cases} 0,&\text{当}\ x=0\ \text{时},\\ 2f(x-1)+x^2,&\text{当}\ x\in\mathbb{Z}^+\ \text{时}.\\ \end{cases} \] 请用 C/C++ 写出此递归函数的代码.

网络学习

网站 www.atzjg.net 主要是关于数学的, 同时也有一些计算机方面的资料. 在首页上可以点击 Register 注册(找到自己的班级, 以学生身份注册).

项目

使用AI创建一个类

使用AI创建一个类

下面使用 DeepSeek 创建一个 Point 类.

输入: 定义一个 Point 类, 其成员有 x,y,z 三个坐标, 类型 double.

DS: 下面是一个 Point 类的完整定义,包含三个 double 类型的坐标成员,并提供基本的构造函数、访问函数以及重载的输出运算符,方便直接输出点坐标。

#include <iostream>

class Point {
private:
    double x, y, z;   // 三个坐标,私有成员

public:
    // 默认构造函数,将坐标初始化为 0.0
    Point() : x(0.0), y(0.0), z(0.0) {}

    // 带参数的构造函数,允许在创建时指定坐标
    Point(double x_val, double y_val, double z_val) 
	  : x(x_val), y(y_val), z(z_val) {}

    // 拷贝构造函数(可选,编译器自动生成的通常足够)
    Point(const Point& other)
	  : x(other.x), y(other.y), z(other.z) {}

    // 获取坐标值的函数(getter)
    double getX() const { return x; }
    double getY() const { return y; }
    double getZ() const { return z; }

    // 设置坐标值的函数(setter)
    void setX(double x_val) { x = x_val; }
    void setY(double y_val) { y = y_val; }
    void setZ(double z_val) { z = z_val; }

    // 重载输出运算符,方便使用 cout << point
    friend std::ostream& operator<<(std::ostream& os, const Point& p) 
    {
        os << "(" << p.x << ", 
                      " << p.y << ", 
                      " << p.z << ")";
        return os;
    }
};

// 使用示例
int main() {
    Point p1;                // 默认点 (0, 0, 0)
    Point p2(1.5, -2.3, 4.8); // 自定义点

    std::cout << "p1 = " << p1 << std::endl;
    std::cout << "p2 = " << p2 << std::endl;

    p1.setX(10.0);
    p1.setY(20.0);
    p1.setZ(30.0);
    std::cout << "after set, p1 = " << p1 << std::endl;

    return 0;
}

使用AI创建派生类

使用AI创建派生类

提示词

现在定义两个类 Position2D 和 Postion3D, 分别用于表示平面上和三维空间中的点. 当然可以使用简单的 struct 来定义. 然后定义 Point2D 和 Point3D 这两个类, 分别表示平面和三维空间中的点, 含有属性 name, fillColor 和 radius, 其中 name 用于存储点的名称, 而 fillColor 和 radius 用来绘制点.

DeepSeek 的思考

我们被要求定义四个类:Position2D、Position3D(作为点的坐标结构),以及 Point2D、Point3D(包含名称、颜色、半径等属性)。题目说“可以使用简单的 struct 来定义” Position2D 和 Position3D。所以我们可以用 struct 来定义位置坐标,而 Point2D 和 Point3D 是包含这些位置作为成员(或者通过继承?)的问题描述说“分别表示平面和三维空间中的点”,并含有属性 name, fillColor 和 radius。而且需要包含位置信息。 有两种常见设计: Point2D 包含一个 Position2D 对象(组合)。 Point2D 继承自 Position2D(继承)。 通常,对于表示点的类,可能选择组合,因为“点”拥有坐标,并且还拥有名称、颜色等属性。如果使用继承,则是“是一个”关系,但坐标是基类,Point2D 是派生类,也可以。但题目要求“定义两个类 Position2D 和 Postion3D,分别用于表示平面上和三维空间中的点”,然后“定义 Point2D 和 Point3D 这两个类,分别表示平面和三维空间中的点”。这里 Position2D 和 Point2D 都表示点,但后者多了属性。那么 Point2D 可以继承 Position2D,增加属性;或者包含一个 Position2D 成员。题目说“当然可以使用简单的 struct 来定义”,似乎暗示 Position 是纯数据,而 Point 是更丰富的类。 为了清晰和复用,通常采用组合。但继承也可以,但要注意如果 Position 是 struct,那么 Point 继承它也是可以的。但是,Point3D 继承 Position3D,则 Point3D 自动拥有 x,y,z 坐标。不过,继承意味着“Point3D is-a Position3D”,逻辑上也合理,因为 Point3D 是一个带有额外属性的三维点。 从命名看,“Position2D” 强调位置坐标,而 “Point2D” 是更具体的点(有名字、颜色等)。我认为继承是合理的,这样可以直接使用坐标成员。如果使用组合,则需要通过成员对象访问坐标,稍微麻烦一点。 但是要注意,如果采用继承,那么 Position2D 应该提供公共的 x,y 成员(或访问函数)。既然是 struct,通常直接公开数据成员。这样派生类也可以直接访问这些成员。 我们按继承来设计。定义:

struct Position2D {
double x, y;
// 可以添加构造函数
Position2D(double x=0, double y=0) : x(x), y(y) {}
};

struct Position3D {
double x, y, z;
Position3D(double x=0, double y=0, double z=0) : x(x), y(y), z(z) {}
};

然后定义 Point2D 继承 Position2D,并增加 name, fillColor, radius。同样 Point3D 继承 Position3D。 这样 Point2D 可以直接访问 x,y;同时拥有自己的属性。 另外,可能需要提供一些成员函数,如设置/获取坐标、属性,以及输出等。题目没有明确要求,但可以添加方便的函数。 我们编写完整的类定义,并可能添加输出运算符。 注意:name 是字符串,fillColor 也是字符串,radius 是 double。 为了符合“使用简单的 struct 来定义” Position,我们确实用 struct。但 Point 可以用 class,或者也使用 struct。根据习惯,可以都用 struct,因为主要是数据聚合。但为了体现封装性,或许将数据设为私有并提供访问函数?但题目没有强制。考虑到简单性,我们可以让 Point 也是 struct,所有成员公开。但 name, fillColor, radius 也是公开?可以公开,但可能希望有封装。题目没有具体要求,我们就用 struct 简单定义所有成员公开,但可以添加构造函数和输出函数。在 C++ 中 struct 和 class 几乎一样,只是默认访问权限不同。我们使用 struct 便于直接访问成员。 但为了保持一点封装性,可以将 name, fillColor, radius 设为私有,并提供 get/set?但题目没要求,简单起见,都公开吧,方便使用。 我们最终提供四个类型的定义,并给出示例。 注意:Position2D 和 Position3D 是独立的,没有继承关系。Point2D 继承 Position2D,Point3D 继承 Position3D。 还需要注意:Position 中的坐标是 double 类型。 我们编写代码。

DeepSeek 的实现

根据要求,使用 struct 定义简单的坐标类 Position2D 和 Position3D,然后定义 Point2D 和 Point3D 分别继承对应的坐标类,并添加名称(name)、填充色(fillColor)和半径(radius)属性。所有成员均设为公有以便直接访问,同时提供构造函数和输出运算符以方便使用。

#include <iostream>
#include <string>

// 二维坐标(简单结构)
struct Position2D {
    double x, y;
    Position2D(double x = 0.0, double y = 0.0) 
	  : x(x), y(y) {}
};

// 三维坐标(简单结构)
struct Position3D {
    double x, y, z;
    Position3D(double x = 0.0, double y = 0.0, double z = 0.0)
	  : x(x), y(y), z(z) {}
};

// 二维点(继承 Position2D,增加名称、颜色、半径)
struct Point2D : public Position2D {
    std::string name;
    std::string fillColor;
    double radius;

    // 默认构造函数
    Point2D() : Position2D(), name(""), fillColor("none"), radius(0.0) {}

    // 带参构造函数
    Point2D(double x, double y, const std::string& n, const std::string& color, double r)
        : Position2D(x, y), name(n), fillColor(color), radius(r) {}

    // 拷贝构造函数
    Point2D(const Point2D& other)
        : Position2D(other), name(other.name), fillColor(other.fillColor), radius(other.radius) {}

    // 重载输出运算符
    friend std::ostream& operator<<(std::ostream& os, const Point2D& p) {
        os << "Point2D \"" << p.name << "\" at (" << p.x << ", " << p.y
           << "), color: " << p.fillColor << ", radius: " << p.radius;
        return os;
    }
};

// 三维点(继承 Position3D,增加名称、颜色、半径)
struct Point3D : public Position3D {
    std::string name;
    std::string fillColor;
    double radius;

    // 默认构造函数
    Point3D() : Position3D(), name(""), fillColor("none"), radius(0.0) {}

    // 带参构造函数
    Point3D(double x, double y, double z, const std::string& n, const std::string& color, double r)
        : Position3D(x, y, z), name(n), fillColor(color), radius(r) {}

    // 拷贝构造函数
    Point3D(const Point3D& other)
        : Position3D(other), name(other.name), fillColor(other.fillColor), radius(other.radius) {}

    // 重载输出运算符
    friend std::ostream& operator<<(std::ostream& os, const Point3D& p) {
        os << "Point3D \"" << p.name << "\" at (" << p.x << ", " << p.y << ", " << p.z
           << "), color: " << p.fillColor << ", radius: " << p.radius;
        return os;
    }
};

// 简单测试
int main() {
    Point2D p2d(1.5, 2.0, "A", "red", 0.5);
    Point3D p3d(3.0, 4.0, 5.0, "B", "blue", 1.2);

    std::cout << p2d << std::endl;
    std::cout << p3d << std::endl;

    // 直接访问成员
    p2d.x = 10.0;
    p2d.name = "A2";
    p2d.radius = 1.0;
    std::cout << "Modified: " << p2d << std::endl;

    return 0;
}

说明 Position2D / Position3D:使用 struct 定义,仅包含坐标数据,并提供默认构造函数。 Point2D / Point3D:公有继承对应的坐标类,因此可以直接访问 x, y(或 z)成员。新增属性 name(点名称)、fillColor(填充色)、radius(绘制半径)均为公有,便于直接读写。 构造函数支持默认构造和全参数构造,拷贝构造函数也一并提供。 重载了 << 运算符,方便使用 std::cout 输出点的完整信息。 在 main 中简单演示了创建对象、输出和修改成员的操作。 这种设计将坐标与点属性分离,既保持了坐标结构的简洁,又通过继承复用了坐标成员,同时点类可灵活扩展。

End






Thanks very much!

This slide is based on Jeffrey D. Ullman's work, which can be download from his website.