C++ 常用语法

字数 10212阅读 499

C++文件

例:从文件income. in中读入收入直到文件结束,
并将收入和税金输出到文件tax. out。

#include<iostream>
using namespace std;
const int cutoff = 6000;
const float rate1 = 0.3;
const float rate2 = 0.6;
int main()
{
  ifstream infile;
  ofstream outfile;
  int income,tax;
  infile.open("income.in")
  outfile.open("tax.out")
  while( infile>>income){
    if( income<cutoff)
      tax = rate1 * income;
    else
      tax = rate2 * income;
    outfile<< "Income = "<< income
              << "greenbacks\n"
              << "Tax = " << tax
              << "greenbacks\n";
    }
    infile.close();
    outfile.close();
    return 0;
}

检查文件是否成功打开

ifstream infile;
infile.open("scores.dat")
if (infile)
  //...
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
int main()
{
    ifstream infile;
    ofstream outfile;
    infile.open("in.txt");
    outfile.open("out.txt");
    int num1,num2,num3=0;
    if(infile && outfile)
    {

        while(infile >> num1 >> num2 >> num3){
            outfile << setw(2)<< num1<<" "<<num2<<" "<<num3<<" "<<num1+num2+num3<<endl;
        }
    }
    infile.close();
    outfile.close();
    return 0;
}

常量

C++中的const变量能在任何常数可以出现的地方使用,例如数组的大小、case标号中的表达式。

const int Size = 100;
float a[Size];

bool data type

C++新增bool类型,取值true 或false。用来表示真假。
所有的关系操作符、相等操作符和逻辑操作符现在都
产生bool类型的结果值,而不是int型。
在需要bool类型的地方,整数和指针表达式仍然是允
许的
默认情况下,bool表达式输出时真值输出1,假值输出0.
操作符boolalpha可用来将bool表达式输出或输入为false 或true的形式。
操作符noboolalpha可用来将bool表达式输出或输入0或1的形式。

bool flag;
flag = (3<5);
cout<<flag<<'\n';
cout<<boolalpha<<flag<<'\n';

1
true

Structure

C++中的结构体和C语言结构体不同。定义结构体变量时可以不加struct关键字

struct Point{
  double x,y;
};
Point p1,p2;

C++中的结构体除了包含数据成员,还可以包含函数。

struct Point{
  double x,y;
  void setVal(double,double);
};
p.x = 3.14159;
p.y = 0.0;
p.setVal(4.11,-13.090);

在C++中,类和结构的唯一区别是缺省情况下,结构中的所有东西都是Public而类中的所有东西都是Private的.

string 类型

C++提供string类型来替代C语言中以null为结尾的char数组。
使用string类型必须包含头文件string
有了string类型,程序员不再需要关心存储的分配,也无需处理复杂的null结束字符,这些操作将由系统自动处理。
实例:

#include<string>
using namespace std;
string s1;
string s2="Bravo";
string s3=s2;
string s4(10,'x');

变量s1,已经定义但没有进行初始化, 默认值为空串
变量s2的初始值是C风格的字符串“Bravo”
变量s3用s2初始化,因此s2和s3都代表字符串Bravo
变量s4的初始化为10个x。

转换为C风格的字符串:利用函数c_str返回一个指向char类型的数组的指针

实例:
存放输入文件名的变量filename的数据类型是string
调用ifstream的open函数时,需要一个C风格的字符串

string filename = "infile.dat";
ifstream infile;
infile.open( filename.c_str() );

求字符串长度,使用函数length

string s = "Ed Wood";
cout << "Length = " << s.length() <<'\n';

输出为Length = 7.

string的输入输出
<<用来输出string类型的字符串

string s1;
string s2 = "Bravo";
string s3 = s2;
string s4(10, 'x');
cout<<s1<<'\n'
       <<s2<<'\n'
       <<s3<<'\n'
       <<s4<<'\n';

输出为

Bravo
Bravo
xxxxxxxxxx

用来输入string类型的字符串,其默认的动作是忽略空格,然后读取存储字符直到文件结束或遇到另外一个空格。任何空格都不存储。

string s;
cout << "Enter a string:";
cin >>s;
输入
Ed Wood

则s的内容为Ed。
注意:在定义后,s实际上长度为0。在读入字符串Ed后,它的长度为2。系统自动提供了充足的存储空间来存储这个长度为2的字符串。

函数getline:用来读入一整行到string类型的变量中去。第一个参数是输入流,第二个参数是string类型的变量。

该函数从输入流中读入字符,然后将它们存储到string变量中,直到出现以下情况为止:
读入了文件结束标志。
都到了一个新行,该新行将从流中移除,但没有存储到变量中。
到达字符串的最大长度允许值。
如果getline没有读入字符,它将返回false,该条件可用于判断文件是否结束以终止应用程序

实例:

#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
    string buff;
    ifstream infile;
    ofstream outfile;
    cout<<"Input file name:";
    cin>>buff;
    infile.open(buff.c_str());
    cout<<"Output file name:";
    cin>>buff;
    outfile.open(buff.c_str());
    while(getline(infile,buff))
        outfile<<buff<<"\n\n";
    infile.close();
    outfile.close();
    return 0;
}

输出信息的行距是输入信息行距的两倍。

赋值:操作符=可用来进行string类型字符串的赋值
操作符左边必须是一个string类型的字符串,右边可以是一个string字符串,也可以是C风格的字符串或仅仅是一个char字符。
字符串的连接:操作符+和+=可用来进行字符串的连接。
操作符+
string + string
string + "xxxxx" 或 "xxxxx" + string
string + 'x' 或 'x' + string
而+=,则左边必须是string字符串,右边可以是一个
string字符串、C风格的字符串或一个char字符。
–string += string
–string += “xxxx”
–string += ‘x’

函数
应用:通过&来标记,用来为存储器提供别名

int x;
int &ref = x;
//分配了一个int单元,它拥有两个名字:x和ref
x=3;或ref=3;都将3存到int单元

C++默认的调用方式和C语言一样,都是传值调用。如果用&指定一个函数参数为引用参数,则为引用调用,引用参数将实际的实参传给函数,而不是实参的一个拷贝。

#include<iostream>
using namespace std;
void swap(int &,int &);
int main()
{
  int i=7,j=-3;
  swap(i,j);
  cout<<"i="<<i<<'\n'
         <<"j="<<j<<'\n';
  return 0;
}
void swap(int &a,int &b)
{
  int t;
  t = a;
  a = b;
  b = t;
}

函数原型 void swap(int &,int &)指定swap的参数是通过引用传递的。
在swap被调用后,swap函数体中的a和b直接对应main函数中的i和j的存储空间。函数swap并不是对i,j的拷贝进行操作,而是直接操作i和j本身。

函数重载:函数名相同,参数个数或参数类型不一样。
重载函数通常用来对具有相似行为而数据类型不同的操作提供一个通用的名称。编译器通过将实参类型与同名函数的参数表进行匹配,以决定应该调用哪个函数。

#include<iostream>
#include<iomanip>
using namespace std;
void print(int a);
void print(double a);
int main()
{
  int x = 8;
  double y = 8;
  print(x);
  print(y);
  return 0;
}

void print(int a){
  cout<<a<<'\n';
}
void print(double a)
{
  cout<<showpoint<<a<<'\n';
}

函数签名:C++要求重载的函数具有不同的签名。
函数签名包括:函数名。参数的个数、数据的类型和顺序。
为保证函数的唯一性,函数必须拥有独一无二的签名。
返回值类型不是函数签名的一部分,所以函数不能通过返回值类型加以区分。

new和delete操作符

new new[] delete 和 delete[]操作符用于动态分配和释放存储空间(例如程序运行的时候)
操作符new分配一个空间;
new[]分配一个数组;
delete释放由new分配的单一空间;
delete[]释放由new[]分配的数组;
与C语言中的函数malloc calloc 和free不同
new、new[] delete delete[]是内建的操作符,而malloc calloc free是库函数。new和delete是关键字

new操作符根据请求分配的类型推断返回类型和需要分配的字节数。
给定声明

int *int_ptr;

通常使用如下方式为int_ptr分配存储空间;
int_ptr = new int;
如果分配成功,则int_ptr指向所分配的存储单元。
delete操作符用于释放由new分配的存储空间。如果int_ptr指向一个由new分配的单一int单元,则可以这样释放它

delete int_ptr;

new[]操作符用于动态分配一个数组

int *int_ptr;
int_ptr = new int[100];//请求分配100个int类型单元

如果分配成功,则int_ptr指向第一个int单元的地址

delete[]操作符用于释放由new[]分配的存储空间。如果int_ptr指向一个由new[]分配的int数组单元,则我们可以这样释放它:

delete [] int_ptr;

C++中,一个类就是一种数据类型。
标准C++定义了一些内建类,例如string
通过创建自己的类,程序员可以对C++语言进行扩展。
通过类声明可以创建一个类,而且可将这个类当作数据类型来使用。

类和对象
类声明:描述了封装在该类中的数据成员和成员函数。

class Human{
       ///...data members and methods go here
};

class是个关键字,Human称为类标签
通过类声明创建一个数据类型,类标签是该数据类型的标识符或名字。
类声明中花括号后的分号不可少。
对象定义:从面向对象程序设计角度看,C++中以一个类作为数据类型定义的变量就是对象。
Human maryLeakey;// 如下语句定义了Human的一个对象maryLeakey
对象数组 Human latvians[365000];
C++的信息隐藏机制
三个关键字:
private:可用来隐藏类的数据成员和成员函数
public:用来暴露类的数据成员和成员函数
protected

面向对象设计的灵魂就是使用private隐藏类的实现,使用public暴露类的接口。
定义一个Person类
接口:包含两个公有成员函数setAge和getAge
实现:一个unsigned 类型的数据成员age

class Person{
  public:
    void setAge(unsigned n);
    unsigned getAge() const;
  private:
    unsigned age;
};

private成员和public成员可以在类声明中交叉出现。
Person类的客户(指Person类的对象的使用者)可通过调用公有成员函数setAge和getAge来请求Person类提供服务
Person类的客户不能访问属于类实现部分的私有数据成员age
成员选择符

class Person{
  public:
    void setAge(unsigned n);
    unsigned getAge() const;
  private:
    unsigned age;
};
int main()
{
  Person boxer;
  boxer.setAge(27);
  //...remainder of main's body

对象的使用者只能访问类的公有成员(数据成员或成员函数)
类范围
类的私有成员仅能由类的成员函数访问,即具有类范围性质。
类的公有成员拥有公有范围性质,可以在类之外进行访问。
在C++中,用关键字class声明的类,其类成员在默认情况下作为私有成员处理,具有类范围性质。
关键字class和struct的区别
使用class关键字或struct关键字都可以创建类
如果使用class关键字,类成员在默认状态下是私有的。
而是用struct关键字,类成员在默认状态下是公有的。

类成员函数定义

在类声明之外定义、在类声明之中进行定义(inline)

class Person{
public:
   void setAge(unsigned n);
   unsigned getAge() const;
private:
   unsigned age;
};

//define Person's setAge
void Person::setAge(unsigned n){
   age = n;
}
//define Person's getAge
unsigned Person::getAge() const{
   return age;
}

在类声明之外进行定义,为避免重名,在定义成员函数时使用了域解析符::

在类声明之中进行定义(inline)

class Person{
public:
    void setAge(unsigned n){ age = n;}
    unsigned getAge() const{return age;}
private:
    unsigned age;
};

通过在进行成员函数声明的时候使用inline关键字,可将原本定义在类声明之外的成员函数强制变成内联函数。

class Person{
public:
    inline void setAge(unsigned n);
    inline unsigned getAge() const;
private:
    unsigned age;
};

//define Person's setAge
void Person::setAge(unsigned n){
    age = n;
}
//define Person's getAge
unsigned Person::getAge() const{
    return age;
}

在程序中使用类
关键步骤:类声明,对象定义,客户服务请求

#include<iostream>
using namespace std;

class Person{

public:
    void setAge(unsigned n){age = n;}
    unsigned getAge() const{ return age;}

private:
    unsigned age;
};

int main()
{
    Person p;  //create a single person
    Person stooges[3]; //create an array of Persons
    p.setAge(12);
    //set the stooges' age
    stooges[0].setAge(45);
    stooges[1].setAge(46);
    stooges[2].setAge(44);
    //print four ages

    cout<<p.getAge()<<'\n';
    for(int i=0;i<3;i++)
        cout << stooges[i].getAge() << '\n';
    return 0;
}

在程序中使用类
通常将类声明放到.h中,这样在使用时通过#include将类声明包含进来。
如可将Person类的声明放到person.h文件中
通常将成员函数的定义放到.cpp中
一般不要将成员函数定义放在.h中,因为头文件通过#include被多个不同的文件所包含的话可能出现函数重复定义错

实例程序:堆栈类
问题:创建一个支持int型的压入和弹出操作的堆栈类。
公有成员:
对stack对象进行初始化。
检查stack为空,或已满。
将整数压入到stack中。
从stack里弹出整数。
不移出任何元素,将stack的内容输出到标准输出。
私有成员:
一个用于打印错误信息的私有成员函数。
三个私有数据成员(top、数据数组、dummy_val)

#include<iostream>
using namespace std;

class Stack{
public:
    enum{ MaxStack = 5};
    void init(){ top = -1;}
    void push(int n) {
        if(isFull()){
            errMsg("Full stack. Can't push.");
            return;
        }
        arr[++top] = n;
    }
    int pop() {
        if( isEmpty()){
            errMsg("Empty stack. Popping dummy value.");
            return dummy_val;
        }
        return arr[top--];
    }
    bool isEmpty(){ return top<0;}
    bool isFull() { return top>=MaxStack -1;}
    void dump() {
        cout<<"Stack contents, top to bottom:\n";
        for(int i=top;i>=0;i--)
            cout<<'\t'<<arr[i]<<'\n';
    }

private:
    void errMsg(const char* msg) const{
        cerr<< "\n*** Stack operation failure:"<<msg<<'\n';
    }

    int top;
    int arr[MaxStack];
    int dummy_val;
};

int main()
{
    Stack s1;
    s1.init();
    s1.push(9);
    s1.push(4);
    s1.dump(); // 4 9
    cout << "Popping"<<s1.pop()<<'\n';
    s1.dump(); //9
    s1.push(8);
    s1.dump(); // 8 9
    s1.pop();s1.pop();
    s1.dump();//empty
    s1.pop();//still empty
    s1.dump(); //ditto
    s1.push(3);
    s1.push(5);
    s1.dump();//5 3
    //push two too manny to test
    for(unsigned i = 0;i<Stack::MaxStack;i++)
        s1.push(1);
    s1.dump(); //1 1 1 5 3
    return 0;
}

效率和健壮性

  • 通过引用来传递和返回对象
  • const类型参数的对象引用
  • const成员函数
  • 对成员函数进行重载以便处理两种类型的字符串

通过引用来传递和返回对象
对象可以采用传值方式或引用方式进行对象的传递和返回。一般来说应该采用引用方式进行对象的传递和返回,而不要采用传值的方式来进行。因为通过传值方式来传递和返回对象时会降低效率并将面临对象间的拷贝操作,从而使数据增大,浪费内存。
从效率上看,传递一个指向对象的指针可收到与引用方式相同的效果,但引用方式的语法要简练得多。

#include<iostream>
using namespace std;

class C{
public: 
    void set(int n){num = n;}
    int get() const{return num;}

private:
    int num;
};

void f(C&);
C& g();

int main()
{
    C c1,c2;
    f(c1); // pass by reference
    c2 = g(); // return by reference
    cout<< c2.get() <<'\n';
    return 0;
}

void f(C& c){
    c.set(-999);
    cout<<c.get()<<'\n';
}

C& g(){
    static C c3;// NB:static, not auto
    c3.set(123);
    return c3;
}

output:
-999
123 

const类型参数的对象引用
通常,如果一个对象通过引用方式传到函数f中,而函数f又不会通过修改对象的数据成员的值改变该对象的状态,那么,最好将f的参数标记为const,可以预防对参数的误写,同时有些编译器还可对这种情况进行一些优化。
如下例:将函数setName的string类型参数n标记为const,表明setName不会改变n,只是将n赋值给数据成员name。

class C{
public:
    void setName(const string& n) {name = n;}
    // ... other public members
private:
    string name;
};

const成员函数
如果一个成员函数不需要直接或间接(通过调用其它的成员函数来改变其对象状态)地改变该函数所属对象的任何数据成员,那么最好将这个成员函数标记为const。
如下例,由于get成员函数不需要改变类C的任何数据成员,因此将get成员函数标记为const。

class C{
public:
    void set(int n) {num = n;}
    int get() const {return num;}
private:
    int num;
};

const 成员函数
定义一个const成员函数时,const关键字出现在参数列表与其函数体之间。
由于get成员函数不更改任何数据成员,因此这种类型的函数被称为只读函数。将成员函数标记为const可以预防对该函数所属对象的数据成员的误写,同时有些编译器还可对这种情况进行一些优化。
一个const成员函数仅能调用其它const成员函数,因为const成员函数不允许直接或间接地改变对象的状态,而调用非const成员函数可能会间接改变对象的状态

class C{
public:
    void m1(int x) const{
        m2(x); //*** error: m2 not const
    }
    void m2(int x) { dm = x;}
private:
    int dm;
};

const成员函数
const关键字三种不同用法示例:

  • 在成员函数set中,因为set不该变string类型参数n,n被标为const。
  • 成员函数get返回数据成员name的一个const型引用,此处的const表明谁也不能通过这个引用来修改数据成员name的值。
  • 成员函数get本身被标记为const,因为get不会改变类C唯一的数据成员name的值。
class C{
public:
    void set( const string& n) {name = n;}
    const string& get() const{ return name;}
private:
    string name;
};

const返回

  • 某函数如果采用const返回,则其返回值只能赋给一个const类型的局部变量。
  • 如果该const返回值是一个类的指针或者引用的话,则不能用该指针或引用调用该类的non-const成员函数,因为这些函数可能会改变该类的数据成员的值。
class Foo{
public:
    /*
     * Modifies m_widget and the user may modify the reurned widget.
    */
    Widget *widget();
    /*
     * Does not modify m_widget but the user may modify the returned widget.
     */
    Widget *widget() const;

    /*
    * Modifies m_widget, but the user may not modify the returned widget.
    */
    const Widget *cWidget();
    
    /*
     * Does not modify m_widget and the user may not modify the returned widget.
    */
    const Widget *cWidget() const;

private:
    Widget *m_widget;
};

int main()
{
    Foo f;
    Widget *w1 = f.widget();  //fine
    Widget *w2 = f.cWidget(); //error -"cWidget()"
                                              // returns a const value
                                              // and "w2" is not const
    const Widget *w3 = f.cWidget(); //fine
    return 0;
}

对成员函数进行重载以便处理两种类型的字符串

class C{
public:
    void set( const string & n) {name = n;}
    void set( const char* n) { name = n;}
    const string& get() const { return name;}
private:
    string name;
};

C c1;
string s1("Who's Afraid of Virginia Woolf?");
c1.set(s1); // string argument

C c2;
c2.set( "What, me worry?"); //const char*

构造函数和析构函数

有些函数比较特殊,在调用它们时不需要显式地提供函数名,编译器会自动地调用它们。
类构造函数(class constructor, 可以有多个)和类析构函数(class destructor,最多一个)就是这种类型的函数,通常编译器会自动调用这两个函数而不需要我们显式地发出调用动作。

构造函数:是一种与类名相同的成员函数。当创建类的一个实例时(例如,定义一个类的变量时),编译器会自动地调用某个合适的构造函数。下面的例子中,三个成员函数是构造函数,都有着与类相同名称的Person,而且没有返回值类型。

class Person{
public:
    Person();// constructor
    Person( const string & n); // constructor
    Person( const char* n); //constructor
    void setName( const string& n);
    void setName( const char* n);
    const string& getName() const;
private:
    string name;
};

构造函数不能有返回类型,因此void Person();是错误的。
一个类可以拥有多个构造函数,可以对构造函数进行重载。但每个构造函数必须拥有不同的函数签名。
上个例子中,三个构造函数具有不同的函数签名。
第一个没有参数(默认构造函数),第二个参数类型是const string引用(带参数沟槽函数),第三个的参数类型是C风格字符串const char*(带参数构造函数)。

构造函数的使用:

#include "Person.h" //class declaration
int main()
{
    Person anonymous; //default constructor
    Person jc("J.Coltrane"); // parameterized constructor
    //...
}

当创建一个对象时,构造函数会被编译器自动调用。程序员不需要调用构造函数。构造函数主要用来对数据成员进行初始化,并负责其他一些在对象创建时需要处理的事务。构造函数对提高类的健壮性有重要的作用。

之前的stack类没有构造函数,为保证一个stack正确运行,top成员必须初始化为-1.虽然stack提供了init成员函数来完成这个初始化任务,但程序员可能会在创建一个stack对象之后忘了调用init成员函数而出错。可以通过为stack类增加一个默认构造函数,这样当定义一个stack对象时,编译器自动调用其默认构造函数,默认构造函数再调用init成员函数:

class Stack{
    Stack() {init();} //ensures initialization
    //...
};

构造函数最大的特点是:函数名与类名相同,没有返回类型。
除此之外,构造函数的行为与其他函数相同,也可完成如赋值、条件测试、循环、函数调用等功能。
构造函数既可以在类声明之中定义,也可在类声明之外定义。
下例子中,将默认构造函数定义为inline类型,将带参数构造函数定义放到类声明之外。

class Person{
public:
    Person() {name = "Unknown";}
    Person( const string& n);
    Person( const char* n);
    void setName( const string& n);
    void setName( const char* n);
    const string& getName() const;
private:
    string name;
};
Person::Person( const string& n){
    name = n;
}
Person::Person( const char* n){
    name = n;
}

对象数组与默认构造函数:
如果C是一个类,可以定义任意维数的C对象数组;
如果C拥有默认构造函数,数组中每个C对象都会调用默认构造函数。

#include<iostream>
using namespace std;
unsigned count = 0;
class C{
public: 
    C() { cout<<"Creating C"<< ++ count<<'\n';}
};
C ar[1000];
本例输出为
Creating C1
Creating C2
...
Creating C999
Creating C1000

通过构造函数约束对象的创建
C++程序员常常会将部分构造函数设计为私有成员,将另一部分设计为公有成员,以确保在创建对象时进行正确的初始化。
一个私有构造函数与普通的私有成员函数一样,拥有类范围属性,因而不能再类之外进行调用。
提供私有的默认构造函数

class Emp{
public:
    Emp( unsigned ID) {id = ID;}
    unsigned id; //unique id number
private:
    Emp();//*** declared private for emphasis
    //...
};
int main(){
    Emp elvis;//***** ERROR: Emp() is private
    Emp cher(111222333);// OK, Emp(unsigned) is public
    //...
}

不提供默认构造函数:

class Emp{
public:
    Emp( unsigned ID) {id = ID;}
    unsigned id; //unique id number
private:
    //...
};
Emp elvis; //*** ERROR: no public default constructor

当编译器在类声明中找不到任何构造函数时,才会生成一个公有的默认构造函数。如果一个类已经显式地声明了任何构造函数,编译器不生成公有的默认构造函数。

拷贝构造函数

拷贝构造函数 构造函数分为两组:1、默认构造函数,不带参数 2、带参数构造函数,需要参数。
在带参数构造函数中,有两类很重要的构造函数:
拷贝构造函数:创建一个新的对象,此对象是另外一个对象的拷贝品。
转型构造函数:用于类型间的转换,只有一个参数。

拷贝构造函数的原型
必须是引用: Person( const Person&);
Person( Person&);
下面的原型是错误的 Person( Person);
拷贝构造函数可以有多于一个的参数,但是第一个以后的所有参数都必须有默认值。例如:
Person( const Person& p, bool married = false);
如果类的设计者不提供拷贝构造函数,编译器会自动生成一个。它完成如下操作:将源对象所有的数据成员的值注意赋值给目标对象相应的数据成员。
例如:将定类Person没有定义拷贝构造函数,尽管对象orig和clone拥有不同的存储空间,但相应的数据成员具有相同的值。

Person orig("Dawn Upshaw");
Person clone(orig);

什么时候应该为一个类设计一个拷贝构造函数呢?
答:如果一个类包含指向动态存储空间指针类型的数据成员,则就应为这个类设计拷贝构造函数。

class Namelist {
public:
    Namelist() {size = 0;p=0}
    Namelist( const string [], int);
    void set( const string&, int);
    void set( const char*, int);
    void dump() const;
private:
    int size;
    string* p;
};

Namelist:: Namelist( const string s[], int si) {
    p = new string [size = si];
    for (int i = 0 ;i < size ;i++)
        p[i] = s[i];
}

上例中,没有为类Namelist定义拷贝构造函数,则下例中定义d2时将会调用编译器提供的拷贝构造函数,将的
的数据成员拷贝到d2

int main()
{
    string list[] = {"Lab","Husky","Collie"};
    Namelist d1(list,3);
    d1.dump();//Lab,Husky,Collie
    Namelist d2(d1);
    d2.dump();//Lab, Husky,Collie
    d2.set("Great Dane",1);
    d2.dump();//Lab, Great Dane, Colli
    d1.dump(); //***** Caution: Lab, Great Dane, Collie
    return 0;
}

这时,指针d1.p和指针d2.p将指向同一块存储空间

image.png

潜在危险:操作d1时可能会改变d2的内容,反之亦
然。
为了避免发生潜在错误,为Namelist类设计一个满足要求的拷贝构造函数。

Namelist:: Namelist(const Namelist& d)
{
    p = 0;
    copyIntoP(d);
}
void Namelist::copyIntoP( const Namelist& d) {
    delete [] p;
    if (d.p != 0) {
        p = new string[ size = d.size] ;
        for( int i=0; i<size; i++)
            p[i] = d.p[i];
    }
    else {
        p =  0;
        size = 0;
    }
}

禁止对象拷贝
原因:我们知道,采用传值方式将对象传递给一个函数或者返回一个对象时,将进行对象的拷贝操作。但有些对象很大,比如设计一个Windows类,如果进行对象间拷贝的话,非常费空间和时间。因此需要一种机制,能够禁止这种情况的发生。
措施:通常采用将拷贝构造函数设计成私有成员的方式,将禁止对象间的拷贝操作。
禁止对象拷贝:

class C{
public:
    C();
private:
    C( C&);
};
void f(C); //*** call by value
C g(); //*** return by value
int main(){
     C c1,c2;
     f( c1); //***** ERROR C(C&) is private!
     c2 = g(); //***** ERROR C(C&) is private!
     //...
}

void f(C cobj) { /*...*/}
C g() {/*...*/}

上个例子中,将类C的拷贝构造函数的声明放在private区,这样main函数中对f的调用将导致一个严重错误,因为试图将c1以传值方式传递给函数f。
要改正这个错误,我们就需要修改f,将其参数类型改为类C的引用:void f( C& cobj) {/.../} // ok, call by reference
main 函数对g的调用同样导致一个严重错误,因为函数g以传值方式返回一个C对象,就需要类C拥有一个公有的拷贝构造函数,但类C的拷贝构造函数是私有的。
要避免这个错误,必须让g返回类C的引用:
C& g() {/.../} //ok,return by reference

转型构造函数
转型构造函数是一个单参数的构造函数。它可以将一个对象从一种数据类型(由参数指出)转换为另一种数据类型(该构造函数所属的类)

class Person{
public:
    Person() { name = " Unknown"; } //default
    Person( const string& n) {name = n;} //convert
    Person( const char* n) { name = n;} //convert
    //...
private:
    string name;
};

int main()
{
    Person soprano( "Dawn Upshaw");
    //...
}

转型构造函数可替代函数重载机制,假设函数f的参数类型为Person对象: void f(Person p);// declaration
如果以一个string作为参数来调用f:
string s = "Turandot";
f(s); //string, not Person
只要Person类拥有一个将string转型为Person的转型构造函数,那么编译器就在string 对象s上调用它,以此来构造一个Person对象作为f的参数。
我们称上例中的Person类的转型构造函数支持隐式类型转换,也就是说,该构造函数采用隐藏方式将一个string转型为一个Person。之所以说它是隐式的,是因为这个转型动作由编译器来完成,不需要编程人员提供一个明确的转型操作。
隐式类型转换提供了方便,但有时会导致一些无法预料到的错误,而这些错误往往细微得难以察觉。在这种时候,可以关闭这种因转型构造函数的存在而导致的隐式类型转换动作,以保证程序的正确性。C++提供的关键字explicit可以用来关闭系统的隐式类型转换功能。

class Person{
public: 
    // convert constructor marked as explicit
    explicit Person( const string& n) {name = n;}
    //...
};

void f( Person s) { /* note: f expects a Person... */}
int main(){
    Person p("foo"); //convert constructor used
    f ( p); //ok p is a Person
    string b = "bar";
    f( b ); //***** ERROR: no implicit type conversion
    return 0;
}

构造函数初始化程序
对const类型的数据成员进行初始化时不能直接赋值,如下列赋值操作是错误的。

class C{
public:
    C() {
        x = 0; // ok,x not const
        c = 0;  //***** ERROR: c is const
    }
private:
    int x; //nonconst data member
    const int c; // const data member
};

对const类型的数据成员进行初始化时必须为构造函数添加一个初始化列表

class C{
public:
    C() : c(0) { x = -1;}
private:
    int x;
    const int c;// const data member
};

构造函数的初始化段由一个冒号:开始,紧跟在冒号之后的是需要进行初始化的数据成员,然后是由一对小括号括起来的初始值。
初始化列表仅在构造函数中有效,不能用于其他函数。构造函数的初始化列表可以初始化任何数据成员( const or non const)
但const类型的数据成员只能在初始化列表里初始化,而不能用其他办法进行初始化。

class C {
public:
    C() : c( 0 ), x( -1 ) {} //empty body
private:
    int x;
    const int c;//const data member
};

构造函数与操作符 new 和 new[]
当使用动态方式为一个对象分配存储空间时,C++操作符new和new[]比C函数malloc和calloc做的更好。因为操作符new和new[]在分配存储空间的同时,还会调用相应的构造函数,而malloc和calloc无法完成这个任务。

#include<cstdlib> // for malloc and calloc
class Emp {
public:
   Emp() { /*...*/}
   Emp ( const char* name) {/*...*/}
   //...
};
int main()
{
   Emp* elvis = new Emp();  //default
   Emp* cher = new Emp(" Cher");  //convert
   Emp* losOfEmps = new Emp[1000];  //default
   Emp* foo = malloc ( sizeof ( Emp) ); // no constructor
   //...
   return 0;
}

析构函数
创建类的对象时,会自动调用某个合适的构造函数。同样,当对象被摧毁时,也会自动调用一个析构函数。
对象的摧毁出现在如下两种情况:
以某个类作为数据类型的变量超出其作用范围。
用delete操作符删除动态分配的对象。

与构造函数一样,析构函数也是一个成员函数。
对于类C,其析构函数的原型为: ~C();
由于析构函数不带参数,因此不能被重载,这样每个类只能拥有一个析构函数。
与构造函数一样,析构函数也没有返回类型,所以void ~C();是错误的。

#include<iostream>
#include<string>

using namespace std;
class C{
public:
    C() { //default constructor
        name = "anonynous";
        cout<<name<<" constructing.\n";
    }
    C(const char *n) { //parameterized constructor
        name = n;
        cout<< name<< " constructing.\n";
    }
    ~C() { cout<<name<<" destructing.\n";}
private:
    string name;
};

int main() {
    /* 1 */ C c0("hortense"); //parameterized constructor
    {
        /* 2 */ C c1; //default constructor
        /* 3 */ C c2("foo"); // parameterized constructor
        cout<<'\n';
        /* 4 */ } //c1 and c2 destructors called
        /* 5 */ C *ptr = new C(); //default constructor
        /* 6 */ delete ptr; //destructor for the ptr object
        /* 7 */ return 0; //c0 destructor called
    return 0;
}

console:
hortense constructing.
anonynous constructing.
foo constructing.

foo destructing.
anonynous destructing.
anonynous constructing.
anonynous destructing.
hortense destructing.

构造函数和析构函数小结
在创建对象时,类的构造函数负责完成初始化和其它相关操作。
析构函数在对象摧毁时完成相应的清理工作(例如将构造函数分配的资源释放掉)。
建议为每个带有数据成员的类设计一个默认构造函数,如果需要,也要设计其他构造函数和析构函数。

类数据成员和类成员函数
类成员:成员属于类本身,而不属于类的某个对象。
对象成员或实例成员:属于对象的成员。前面讲过的都是对象成员。
使用关键字static可以创建一个类成员。

类数据成员
声明晶态成员的语法:

class Task {
public:
    static unsigned getN() const (return n;}
    //...
private:
    static unsigned n; // count of Task objects
    //...
};

Task类的数据成员n与Task类本身相关,与任何Task对象无关。
由于n是static的,它对整个Task类而言只有一个,而不是每个Task对象都有一个n。
可以利用n来确定当前存在的Task对象的数量。

Task ( const string & ID) {
    setID( ID);
    logFile = "log.dat";
    setST();
    ft = st;  //no duration ye
    n++; //another Task created
}
~Task() {
    logToFile();
    n--; // another Task destroyed
}

类成员与对象成员实例:

image.png

类数据成员除了必须在类声明内部用static进行声明外,还必须在类外进行定义。
定义时可以指定初始值。缺省情况下初始化为0.

class Task {
public:
    //...
private:
   static unsigned n; // count of Task objects
   //...
};
unsinged Task::n = 0; //define static data member

static 数据成员不会影响该类及其对象的sizeof。如下例中表达式sizeof(C)和sizeof(c1)的值都是16。

class C {
    unsigned long dm1;
    double dm2;
    static unsigned long dm3;  //does not impact sizeof (C)
    static double dm4;  //does not impact sizeof(C)
};

类成员函数
除了static数据成员,类还可以有static成员函数。

class Task {
public:
    static unsigned getN() const {return n;}
    //...
private:
    static unsigned n; //count of Task objects
    //...
};

静态成员函数只能访问其他的static成员,包括数据成员和成员函数。而非静态成员函数既可以访问static成员,也可以访问非静态成员。
static成员函数既可以是inline函数,也可以是非inline函数

class Task {
public:
    static unsigned get() {
        setST(); //*****ERROR: NOT static!
        st = time ( 0 ); //***** ERROR: not static!
        return n;   // ok,n is static
    }
    //...
};

访问static数据成员和static成员函数的方式:
通过对象来访问;直接通过类来访问(推荐)

class C{
public :
    static int sVar;
    static void sMeth();
    //...
};
int main(){
    C c1;
    c1.sMeth(); //through an object
    C::sMeth(); //directly and preferred
    unsigned x = c1.sVar; //through an object
    unsigned y = C::sVar; //directly and preferred
    //...
}

在成员函数内定义静态变量
成员函数内的局部变量可以是static的。如果将成员函数内的某个局部变量定义为静态变量,该类的所有对象在调用这个成员函数时将共享这个变量。

class C{
public:
    void m() ; //object method
private:
    int x;  //object data member
};
void C::m() {
    static int s = 0; //*****Caution: 1 copy for all objects
    cout << ++s << '\n';
}
int main() {
    C c1,c2;
    c1.m()  //output 1
    c2.m();  //output 2
    c1.m();  //output 3
    return 0;
}

上个例子中,在成员函数m中定义了一个static变量s,由于s定义在程序块内,它拥有程序块范围,因此它只能在m内部访问。换句话说,该变量只有在函数内部才有效。每调用m一次,s就会相应地增加一次。
因为m是C的成员函数,所以C的所有对象都共享这个静态局部变量。这样,不同对象对m的每一次调用访问的都是同一个s。
相反,对于非静态局部变量x来说,每个C对象都拥有一个x。

指向对象的指针
对象或对象引用使用成员选择操作符,来访问对象的成员。要通过指针来访问成员,必须使用指针操作符->
在C++中,指向对象的指针主要用于两个方面:
指向对象的指针可以作为参数传递给函数,或通过函数返回。
使用操作符new和new[]动态创建对象,然后返回一个指向该对象的指针。

常量指针this

this是一个关键字,this指针只能出现在类的非静态成员函数中。它指向调用该成员函数的那个对象。静态成员函数中不能出现this指针。
this指针不是对象的一部分,所以不影响对象的大小。
当一个非静态成员函数被某对象调用时,编译器将该对象的地址作为一个隐含的参数传给该成员函数,例如下面的函数调用:
myDate.setMonth(3);可以看作
setMonth(&myDate,3);
在成员函数内部,可以通过this指针来获取对象的地址。
大多数情况下,this指针都是隐含使用的。但也可以显式地使用this指针来调用类的成员。例如:

void Date::setMonth( int mn) {
   month = mn;
   this->month = mn;
   (*this).month = mn;
}

上述三条语句是等价的。
this指针通常用来从成员函数中返回当前对象。
return *this;
this指针有时也用来避免自引用
if (&Object != this)
this指针是一个常量,它不能作为赋值、递增、递减等运算的目标对象。此外this只在非static成员函数中用才有效。
与普通函数相比,静态成员函数由于不与任何对象相联系,因此它不具有this指针。由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长。

继承

C++允许从任何已存在的类派生新类,所派生的类被称为派生类(derived class),又称为子类。而已存在的用于派生新类的类被称为基类(base class),又称为父类。
在C++中,一个派生类既可以从一个基类派生,也可以从多个基类派生。
从一个基类派生类的继承被称为单继承。
从多个基类派生类的继承被称为多继承。

C++中继承的语法:
单继承语法:

class 派生类名 : 访问控制 基类名
{
    数据成员和成员函数声明
};

多继承语法:

class 派生类名 : 访问控制 基类名1,访问控制 基类名2,... 访问控制 基类名n
{
    数据成员和成员函数声明
};
image.png

类继承举例:
单继承:
class Bear : public ZooAnimal
{ ... };
多继承:
class Panda : public Bear, public Endagered
{ ... };
其中:
public: 访问控制关键字,指明继承方式是公有继承。
当一个类通过公有继承方式从基类继承时,基类中的公有成员在派生类中也是公有的。
不指明继承方式关键字public时,编译器会默认继承方式为private或protected。
":"用于建立基类与派生类的层次结构。
基类:C++提供的或用户自定义的类。
派生类中可以定义数据成员和成员函数,此外,还继承基类所有成员。

class Pen{
public:
    enum int {Off, On};
    void set_status( ink );
    void set_location(int ,int);
private:
    int x;
    int y;
    ink status;
};

class CPen : public Pen {
public:
    void set_color( int );
private:
    int_color;
};
image.png

继承机制下的私有成员
基类的所有私有成员仅在基类中可见,而在派生类中是不可见的。
但派生类对象会为基类中的所有私有成员分配空间。
如上个例子中,CPen类从Pen类继承了数据成员x,y和status,尽管这些成员在CPen类中是不可见的,但无论何时创建CPen类的对象时,该对象都将获得相应的存储空间来保存x,y和status等数据成员。
尽管在派生类中不能直接访问基类的私有成员,但可以通过间接的方式(调用从基类继承来的公有成员函数)进行。
如上个例子中,x,y和status可以通过成员函数set_location和set_status进行访问。

class Point {
public:
    void set_x( int x1 ) { x = x1; }
    void set_y( int y1 ) { y = y1; }
    int get_x() const {return x;}
    int get_y() const {return y;}
private:
    int x;
    int y;
};
class Intense_point : public Point {
public:
    void set_intensity( int i ) { intensity = i; }
    int get_intensity() const { return intensity;}
private:
    int intensity;
};
image.png

改变访问限制:
使用using声明可以改变成员在派生类中的访问限制。例如:基类中的公有成员一般情况下被继承为公有成员,但使用using声明可将其改为私有成员(或保护成员)

class BC { //base class
public:
    void set_x( float a ) { x = a;}
private:
    float x;
};
class DC : public BC { //derived class
public:
    void set_y(float b) { y = b; }
private:
    flaot y;
    usign BC::set_x;
};

这样就无法直接通过DC类的任何对象调用set_x:

int main() {
    DC d;
    d.set_y(4.31); //OK
    d.set_x(-8.03); // ***** ERROR:set_x is private in DC
    //...
}

如果基类的某个公有成员函数在继承类中不适合,则可以通过using声明将其转变为私有成员函数,从而使它在派生类中隐藏起来。
例如,“有序元素类”从“无序元素类”派生而来,基类“无序元素类”中的某些成员函数可能并不适合“有序元素类”。
如基类中某个成员函数的算法为:在一个无序表中的任意位置插入一个元素。这个算法显然不符合有序表的处理要求。这样的成员函数就应通过using声明将其在派生类中隐藏起来。

名字隐藏
如果派生类添加了一个数据成员,而该成员与基类中的某个数据成员同名,新的数据成员就隐藏了继承来的同名成员。

保护成员
除了私有和公有成员, C++还提供了保护成员。在没有继承的情况下,保护成员和私有成员类似,只
在该类中可见。在公有继承方式下,保护成员和私有成员具有不同性
质:

  • 基类的保护成员在派生类中是可见的。
  • 而基类的私有成员在派生类中是不可见的。


    image.png
image.png
image.png
image.png
image.png

派生类可对从基类继承来的保护成员进行访问,也就是说保护成员在派生类中是可见的。但派生类不能访问一个基类对象的保护成员,因为基类对象属于基类,不属于派生类。
采用public派生
基类的私有成员,在派生类中不可见,基类的私有成员只能被基类的其他成员函数访问(除friend函数)。
基类的保护成员,在派生类可见,基类的保护成员除了能被基类的其他成员函数访问外,还能被类层次结构中的所有成员函数访问。
基类的公有成员,在派生类可见,可被任何函数访问。

image.png

一般来说,应避免将数据成员设计为保护类型
而是采用私有数据成员与相应保护型访问函数结合的模式,便于实现数据隐藏(复杂数据成员例外)。

image.png

继承机制下的构造函数
当创建一个派生类对象时,基类的构造函数被自动调用,用来对派生类对象中的基类部分进行初始化,并完成其他一些相关事务。
如果派生类定义了自己的构造函数,则由该构造函数负责对象中“派生类添加部分”的初始化工作。

image.png
image.png

当基类构造函数的功能对派生类而言不够用的时候,派生类必须定义自己的构造函数。
可以在派生类的构造函数中调用其基类的构造函数(前提是基类拥有构造函数)

image.png

多米诺骨牌效应:在一个层次很深的类层次结构中,创建一个派生类对象将导致派生链中的所有类的构造函数被逐一调用。

image.png
image.png
image.png

如果基类拥有构造函数但没有默认构造函数,那么派生类的构造函数必须显式地调用基类的某个构造函数。

image.png
image.png

一般来说,最好为基类提供一个默认构造函数,不但可以避免出现上述问题,而且并不妨碍派生类构造函数去调用基类的非默认构造函数。
假设基类拥有默认构造函数,而其派生类也定义了一些构造函数,不过派生类的任何构造函数都没有显式地调用基类的某个构造函数。在这种情况下,当创建一个派生类对象时,基类的默认构造函数将被自动地调用。


image.png

以“DC类从BC 类派生”为例,总结如下:

  • 若DC有构造函数而BC没有,当创建DC类的对象时,DC的相应构造函数被自动调用。
  • 若DC没有构造函数而BC有,则BC必须拥有默认构造函数。只有这样,当创建DC类的对象时,才能自动执行BC的默认构造函数。
  • 若DC有构造函数,且BC有默认构造函数,则创建DC类的对象时,BC的默认构造函数会自动执行,除非当前被调用的派生类构造函数在其初始化段中显式地调用了BC的非默认构造函数。
  • 若DC和BC都有构造函数,但BC没有默认构造函数,则DC的每个构造函数必须在其初始化段中显式地调用BC的某个构造函数。只有这样,当创建DC的对象时,BC的构造函数才能获得执行机会。
    在创建派生类对象时,必须显式地或隐式地执行其基类的某个构造函数,因为有时候,派生类的构造函数可能会依赖基类的构造函数来完成一些必要的操作。例如,依赖基类构造函数来完成部分数据成员的初始化。
image.png

继承机制下的析构函数
在类的层次结构中,构造函数按基类到派生类的次序执行,析构函数则按派生类到基类的次序执行,因此,析构函数的执行次序和构造函数的执行次序是相反的。

image.png
image.png

由于每个类至多只有一个析构函数,因此对析构函数的调用不会产生二义性,这样在析构函数中不必显式地调用其他析构函数,这一点和构造函数的调用规则是不同的。
构造函数回顾

  • 在派生类的对象中,由基类中声明的数据成员和函数成员所构成的封装体称为基类子对象。
  • 基类子对象由基类中声明的构造函数进行初始化。
  • 构造函数不能被继承,所以,一个派生类的构造函数必须通过调用基类的某个构造函数来初始化基类子对象。
  • 派生类的构造函数只负责初始化在派生类中声明的数据成员。

推荐阅读更多精彩内容