PKU C++程序设计实习 学习笔记1

第一章 从C走进C++


1.7 内联函数和重载函数

内联函数:函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。

为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。

重载函数:一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。编译器根据调用语句的中的实参的个数和类型判断应该调用哪个函数。

(1) int Max(double f1,double f2) { }
(2) int Max(int n1,int n2) { }
(3) int Max(int n1,int n2,int n3) { }
Max(3.4,2.5); //调用 (1)
Max(2,4); //调用 (2)
Max(1,2,3); //调用 (3)
Max(3,2.4); //error,二义性,既可以类型转换后调用(1),也可以类型转换后调用(2)

1.8 函数缺省参数

C++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值。

函数参数可缺省的目的在于提高程序的可扩充性。即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。



第二章 类和对象初探


2.1 面向对象程序设计方法

面向对象的程序设计方法

PKU C++程序设计实习 学习笔记1_第1张图片

面向对象的四个基本概念:抽象、封装、继承、多态

抽象:将一类客观事物的共同属性归纳出来,形成一个数据结构。将这类事物所能进行的一些行为和操作归纳起来形成函数,这些函数可以来操作具体的数据结构。

继承:将数据结构和算法对应地捆绑在一起,形成类。


2.5 内联成员函数和重载成员函数

内联成员函数

  • inline + 成员函数
  • 整个函数体出现在类定义内部
class B{
  inline void func1();
  void func2() 
  {
  };
};
void B::func1() { }//注意void 和 B 的顺序,即返回值类型和类名的顺序

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

  • 重载成员函数
  • 成员函数 -- 带缺省参数
  • 使用缺省参数要注意避免有函数重载时的二义性

2.6 构造函数

基本概念

成员函数的一种,名字与类名相同,可以有参数, 不能有返回值(void也不行)
作用是 对对象进行初始化,如给成员变量赋初值
       构造函数只是在对象已经占用了存储空间后,在对象的存储空间里做一些初始化工作。对象所占用的存储空间不是构造函数分配的。
如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数。 默认构造函数无参数,不做任何操作
如果定义了构造函数,则编译器不生成默认的无参数的构造函数
对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
        只要有对象生成,不管是以什么形式生成,对象生成的时候都一定会调用构造函数,来对它进行初始化。

可以有多个构造函数


为什么需要构造函数:

构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
有时对象没被初始化就使用,会导致程序出错。

构造函数最好是public的,private构造函数不能直接用来初始化对象


构造函数在数组中的使用

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是一个3元素的指针数组,元素是指针,并不会导致对象的生成。所以只是在调用new的时候调用了构造函数。


2.7 默认构造函数(自己补充的)

基本概念

没有形参、或所有形参都有默认实参的构造函数为默认构造函数。

默认构造函数说明了当定义对象时没有为它提供(显示的)初始化式时应该怎么办。

只要定义一个对象时,没有提供初始化式,就使用默认构造函数。

合成的默认构造函数

即是编译器自动生成的默认构造函数。(注意区分合成的默认构造函数和默认构造函数!!!

一个类只要定义了一个构造函数,编译器就不再生成默认构造函数。(依据是:如果类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。)

合成的默认构造函数使用与变量初始化相同的规则来初始化:
具有类类型的成员通过运行各自的默认构造函数来进行初始化;
内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化;如果对象是定义在局部作用域中,则内置和复合类型的成员不进行初始化。

类通常应定义一个默认构造函数


第三章 类和对象进阶


3.1 复制构造函数

基本概念

只有一个参数,即对同类对象的引用。(个人理解,它也是一种构造函数,用来初始化。)
形如 X::X( X& )或X::X(const X &), 二者选一。后者能以常量对象作为参数
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
如果定义的自己的复制构造函数,则默认的复制构造函数不存在。
不允许有形如 X::X( X )的构造函数。

复制构造函数起作用的三种情况

  1. 当用一个对象去初始化同类的另一个对象时。
    Complex c2(c1);
    Complex c2 = c1; //初始化语句,非赋值语句   c2是由复制构造函数来初始化的。
  2. 如果某函数有一个参数是类 A 的对象,那么该函数被调用时,类A的复制构造函数将被调用。
    class A
    {
      public:
      A() { };
      A( A & a) {
        cout << "Copy constructor called" <可以看到这里a1会调用复制构造函数,不一定等于实参a2;而如果调用的是默认复制构造函数,则a1和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" <这里调用.v的对象肯定要先生成,生成的时候肯定要调用构造函数来初始化,调用的是哪个构造函数呢?调用的是复制构造函数,来初始化。这是个临时对象。  那复制构造函数的形参是什么呢?是Func函数的返回对象。
      //可以看到,临时对象不一定和Func返回的对象相等。因为取决于你编写的复制构造函数。这个和2中的形参和实参不一定相等是一样的。
      return 0;
    }
    输出结果:
    Copy constructor called 4

注:按规范来说,复制构造函数只能做“复制”的工作,而不能做其他如输入输出、修改外部变量(包括静态变量)等事情。
某些编译器(包括你做测试的这两个)假设程序员严格按照上述规范,将一些不必要的复制构造函数过程略去(比如VS2013和Codeblocks2013,因为函数返回的对象给变量后就没有用了,所以就直接给变量了,而不会严格按照标准“复制”一个新的对象给变量然后删掉函数返回对象)。这样可以极大地提高程序运行效率,尤其是当复制构造函数很复杂的时候。然而,若程序员不按照规范写,就会因省略复制构造导致出现一些非预想的情况。
这也提醒大家,本课程例题、作业里出现的在复制构造函数中输出、修改外部变量等只是为了让大家充分理解复制构造函数的特性。在实际应用时,请勿使用类似操作,以防出现不必要的问题。


3.2 类型转换构造函数

  • 目的
    实现类型的自动转换
  • 特点
    只有一个参数
    不是复制构造函数
  • 编译系统会自动调用—>转换构造函数

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;
}
因为类型转换构造函数也是一种构造函数。
比如上面例子中的 Complex c2 = 12; 其实是对c2对象的初始化,此时是把这个类型转换构造函数当作普通的构造函数来使用,因此不会生成临时对象。

3.3 析构函数

基本概念

成员函数的一种:名字与类型相同,在前面加 ‘~’,没有参数和返回值

 一个类最多只有一个析构函数

对象消亡时—>自动被调用。在对象消亡前做善后工作,释放分配的空间等

定义类时没写析构函数, 则编译器生成缺省析构函数。不涉及释放用户申请的内存释放等清理工作

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


析构函数和数组

对象数组生命期结束时—>对象数组的每个元素的析构函数都会被调用


析构函数和运算符 delete

delete 运算导致析构函数调用

Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest; //析构函数调用
------------------------------------------------------------------
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次


构造函数和析构函数调用时机的例题

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

每个对象都是在其作用域结束时被释放的。

如果两个或多个对象在同一处结束作用域,则“先声明的后释放”。

局部变量比静态变量先释放的原因是局部变量的作用域先结束。


3.4 静态成员变量和静态成员函数

基本概念

静态成员:在说明前面加了static关键字的成员。

普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享

普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用与某个对象

因此静态成员不需要通过对象就能访问。

sizeof 运算符不会计算静态成员变量。
class CMyclass { int n;static int s; }; 则 sizeof( CMyclass ) 等于 4

静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。

静态成员函数本质上是全局函数。

设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。


如何访问静态成员

1) 类名::成员名
CRectangle::PrintTotal();
2) 对象名.成员名
CRectangle r;  r.PrintTotal();//这只是一种形式,并不意味着作用于对象r
3) 指针->成员名
CRectangle * p = &r;  p->PrintTotal();
4) 引用.成员名
CRectangle & ref = r;  int n = ref.nTotalNumber;

静态成员示例

考虑一个需要随时知道矩形总数和总面积的图形处理程序。可以用全局变量来记录总数和总面积。用静态成员将这两个变量封装进类中,就更容易理解和维护。

class CRectangle {
private:
 int w, h;
 static int nTotalArea;
 static int nTotalNumber;
public:
 CRectangle(int w_,int h_);
 ~CRectangle();
 static void PrintTotal();
};
CRectangle::CRectangle(int w_,int h_) {
 w = w_;
 h = h_;
 nTotalNumber ++;
 nTotalArea += w * h; }
CRectangle::~CRectangle() {
 nTotalNumber --
;
 nTotalArea
-= w * h;
}
void CRectangle::PrintTotal() {
 cout << nTotalNumber << "," << nTotalArea << endl; }
int CRectangle::nTotalNumber = 0;
int CRectangle::nTotalArea = 0;
// 必须在定义类的文件中对静态成员变量进行一次说明
//或初始化。否则编译能通过,链接不能通过。
int main()
{
CRectangle r1(3,3), r2(2,2);
//cout << CRectangle::nTotalNumber; // Wrong , 私有
CRectangle::PrintTotal();
r1.PrintTotal();
return 0;
}
输出结果:
2,13
2,13

要注意一点,就是在C++里面啊,静态成员变量你必须拿到外面,就是所有的函数外面来单独的给它声明一下。在声明的同时,你可以对它进行初始化,也可以不初始化。

在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

void CRectangle::PrintTotal()
{
cout << w << "," << nTotalNumber << "," << nTotalArea << endl; //wrong
}
CRetangle::PrintTotal(); //解释不通,w 到底是属于那个对象的?
附:此CRectangle类写法有缺陷。即在调用默认的复制构造函数时没有对总个数增加,而调用析构函数时总个数有减少。解决方法就是,自己定义一个复制构造函数。

3.5 成员对象和封闭类

成员对象: 一个类的成员变量是另一个类的对象

包含 成员对象 的类叫 封闭类 (Enclosing)

class CTyre { //轮胎类
  private:
    int radius; //半径
    int width; //宽度
  public:
    CTyre(int r, int w):radius(r), width(w) { }
};
class CEngine { //引擎类
};
class CCar { //汽车类,即是“封闭类”
  private:
    int price; //价格
    CTyre tyre;
    CEngine engine;
  public:
   CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int w):price(p), tyre(tr, w){};
int main(){
  CCar car(20000,17,225);
  return 0;
}
如果 CCar 类不定义构造函数, 则CCar car; // error  编译出错
编译器不知道 car.tyre 该如何初始化
car.engine 的初始化没有问题: 用默认构造函数。因为它没有成员变量。

生成封闭类对象的语句—>明确 “对象中的成员对象”


封闭类构造函数的初始化列表

定义封闭类的构造函数时, 添加 初始化列表:
类名::构造函数(参数表): 成员变量1(参数表), 成员变量2(参数表), …
{
   …
}

成员对象初始化列表中的参数

任意复杂的表达式
函数 / 变量/ 表达式中的函数, 变量有定义

调用顺序

当封闭类对象生成时,

  • S1: 执行所有成员对象 的构造函数 
  • S2: 执行 封闭类 的构造函数

成员对象的构造函数调用顺序

  • 和成员对象在类中的说明顺序一致 
  • 与在成员初始化列表中出现的顺序无关

当封闭类的对象消亡时,

  • S1: 先执行 封闭类 的析构函数 
  • S2: 执行 成员对象 的析构函数

析构函数顺序和构造函数的调用顺序相反


封闭类例子程序

class CTyre {
  public:
    CTyre() { cout << "CTyre contructor" << endl; }
    ~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
  public:
   CEngine() { cout << "CEngine contructor" << endl; }
   ~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar 
{
  private:
    CEngine engine;
    CTyre tyre;
  public:
    CCar( ) { cout << “CCar contructor” << endl; }
    ~CCar() { cout << "CCar destructor" << endl; }
};
int main()
{
  CCar car;
  return 0;
}
程序的输出结果是:
CEngine contructor
CTyre contructor
CCar contructor
CCar destructor
CTyre destructor
CEngine destructor

3.6 友元(个人觉得,友元的设置就是让它能够访问私有成员)

  • 友元函数
一个类的 友元函数可以访问该类的 私有成员
class CCar; //提前声明 CCar类, 以便后面CDriver类使用
class CDriver {
  public:
    void ModifyCar( CCar * pCar) ; //改装汽车
};
class CCar {
  private:
    int price;
  friend int MostExpensiveCar( CCar cars[], int total); //声明友元
  friend void CDriver::ModifyCar(CCar * pCar); //声明友元
}; 
void CDriver::ModifyCar( CCar * pCar)
{
  pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar( CCar cars[], int total) //求最贵汽车的价格
{
  int tmpMax = -1;
  for( int i = 0; i < total; ++i )
   if( cars[i].price > tmpMax)
     tmpMax = cars[i].price;
  return tmpMax;
}
int main()
{
  return 0;
}
将一个类的成员函数(包括 构造, 析构函数)  设置为另一个类的友元
class B {
  public:
    void function();
};
class A {
  friend void B::function();
};

  • 友元类
A是B的 友元类—>A的成员函数可以访问B的私有成员
class CCar {
 private:
   int price;
 friend class CDriver; //声明CDriver为友元类
};
class CDriver {
 public:
   CCar myCar;
   void ModifyCar() { //改装汽车
     myCar.price += 1000; // CDriver是CCar的友元类可以访问其私有成员
   }
};
int main() { return 0; } 
Note:友元类之间的关系不能传递, 不能继承

3.7 this 指针

C++程序到C程序的翻译

class CCar
{
  public:
    int price;
    void SetPrice(int p);
};
void CCar::SetPrice(int p)
{ 
  price = p; 
}
int main() {
  CCar car;
  car.SetPrice(20000);
  return 0;
}
翻译到 C 语言:
struct CCar {
  int price;
};
void SetPrice(struct CCar * this,int p)
{ 
  this->price = p; 
}
int main() {
  struct CCar car;
  SetPrice( & car,20000);
  return 0;
}
成员函数的翻译:成员函数会被翻译成一个全局的函数。然後这个全局函数参数个数要比成员函数多一个。多出来的这个参数,就是所谓的一个this指针。那这个this指针指向谁呢?就指向在C++程序里面那个成员函数所作用的这个对像。 

实际上你完全可以这样理解。就是C++的编译,你就完全可以把它理解成先翻译成C,然後再拿C的编译去编译。

this指针作用

其作用就是指向成员函数所作用的对象
非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。
class A
{
  int i;
  public:
    void Hello() { cout << "hello" << endl; }
};  —> void Hello(A * this ) { cout << "hello" << endl; }
int main()
{
  A * p = NULL;
  p->Hello();//结果会怎样?  —> Hello(p);
} // 输出:hello
class A
{
  int i;
  public:
    void Hello() { cout <i << "hello"<< endl; }
//this若为NULL,则出错!!
int main()
{
  A * p = NULL;
  p->Hello();
Hello(p);
}

this指针和静态成员函数

静态成员函数中不能使用 this 指针!因为静态成员函数并不具体作用与某个对象!
因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数!而普通的成员函数它的真实的参数个数是比你写出来的要多一个的,多出来的这个实际上就是this指针。

3.8 常量对象、常量成员函数和常引用

常量对象

如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加const关键字。
class Demo{
  private :
    int value;
  public:
    void SetValue() { }
};
const Demo Obj; // 常量对象

常量对象不能修改它的成员变量值
常量对象不能调用非常量成员函数(静态成员函数除外)  见下面例子的两个错误。


常量成员函数

在类的成员函数说明后面可以加 const关键字,则该成员函数成为 常量成员函数
常量成员函数执行期间 不应修改其所作用的对象
因此,在常量成员函数中不能修改成员变量的值( 静态成员变量除外),也不能调用同类的非常量成员函数( 静态成员函数除外)。
class Sample
{
  public:
    int value;
    void GetValue() const;
    void func() { };
    Sample() { }
};
void Sample::GetValue() const
{
  value = 0; // wrong
  func(); //wrong
}
int main() {
  const Sample o;
  o.value = 100; //err.常量对象不可被修改
  o.func(); //err.常量对象上面不能执行非常量成员函数 编译器不会去分析func函数到底做了什么,它只看到o是常量对象,而func是非常量成员函数,func是有可能改变对象的成员变量的。
  o.GetValue(); //ok,常量对象上可以执行常量成员函数
  return 0;
} //在Dev C++中,要为Sample类编写无参构造函数才可以,Visual Studio2010中不需要

常量成员函数的重载

两个成员函数,名字和参数表都一样,但是一个是const,一个不是,算重载。

常引用

  • 引用前面可以加const关键字,成为常引用。不能通过常引用,修改其引用的变量。
  • 对象作为函数的参数时,生成该参数需要调用复制构造函数,效率比较低。用指针作参数,代码又不好看,如何解决?
可以用对象的引用作为参数,如:
class Sample {
  …
};
void PrintfObj(Sample & o)
{
  ……
}
对象引用作为函数的参数有一定风险性,若函数中不小心修改了形参o,则实参也跟着变,这可能不是我们想要的。如何避免?
可以用对象的常引用作为参数,如:
class Sample {
  …
};
void PrintfObj( const Sample & o)
{
  ……
}//这样函数中就能确保不会出现无意中更改o值的语句了。



你可能感兴趣的:(C/C++)