学习目标

  • 了解define与const的区别。
  • 掌握const的不同应用。
  • 掌握引用拷贝构造函数的使用方法。

    一、const使用方法

    声明格式:
    image.png
    例如: const int MAX=100;
    可以用MAX来定义数组的长度,例如:int iDataList[MAX];
    注意:
    (1) 尽量把const定义放进头文件里。
    (2) 当定义一个常量(const)时,必须初始化,即赋初值给它,除非用extern做了清楚的说明:
    extern const int bufsize;
    例:
    在文件1.h中有以下声明:
    //1. hconst int bufsize=100;
    在2.cpp中需要用到常量bufsize:
    //2. cppextern const int bufsize;
    [例4-1] 常数组与常对象。

    1. const int Datalist[] = {5,8,11,14};
    2. Struct Mystruct {int i; int j;}
    3. const struct Mystruct sList[] = {{1,2},{3,4}};
    4. char cList[Datalist[1]]; //错误
    5. float fList[sList[0].i]; //错误
    6. 错误的原因是因为,在编译时编译器必须为数组分配固定大小的内存空间。而使用const修饰的数组意味着“不能被改变”的一块存储区,其值在编译期间不能被使用。

    const与指针

    const与指针的结合使用,有两种情况: 一是const修饰指针,即修饰存储在指针里的地址; 二是修饰指针指向的对象 采用“靠近”原则,即const离哪个进就是修饰哪个。**

    1.指向常量的指针

    定义格式:
    image.png
    例如:
    const int p;表明p是一个指针,是一个指向const int的指针。即p指向一个整型常量,这个常量当然不能被改变,但是p可以“被改变”。它的另一种形式为:int const p;

    2.常指针

    定义格式:
    image.png
    例如:
    int i=4;
    int const q=&i;
    表明q为一个常指针,一个指向int类型的变量i的const指针,q必须有一个初始值,它只能指向这个初始对象i,不能“被改变”而指向其他对象,但对象的值可以被改变。
    例如:
    i=5;
    q=6;
    常指针q指向整型变量i,i的初值为4,通过赋值语句“i=5”变为5,又经赋值语句“*q=6”而改变为6。
    int i=4,j=5;

  • 指向常量的指针

const int q = &i
<=>int const
q = &i
<=>q=&j合法
<=>*q=1非法

  • 常指针

int const q=&i
<=>q=&j非法
<=>
q=1合法

Const标准不能下降

本来可变现在不变 ———标准升高,可以
本来不变现在可变 ———标准下降,报错
是否为常指针并不影响
情况一:i为变量
int i=4;
int p1=&i;
int
const cp1=&i; //可以用指针或常指针指向一个普通对象(非常对象)
const int p2=&i;
const int
const cp2=&i; //可以把普通对象(非常对象)地址赋值给常对象指针,标准升高
情况二:i为常量
const int j=5;
int const cp3=&j; //报错!!!标准下降!!!
int
p3= &j; //报错!!!标准下降!!!

注意:
int const cp6=(int)&j;
int p6=(int)&j; //强制转换,合法,但是不建议这样使用

注意:
char *p4= “hello!”; //可以用指向字符的指针指向字符串,但会警告
char p5[]= “hello!”; //推荐

3.const与引用

使用引用和指针一样,目的都是提高程序的执行效率,const旨在提高安全性。在常量修饰引用时,同样要注意“标准不下降(一致性)”的问题,
例如:
const int j=3;
const int& c=j;
int& k=j; //错误,标准下降,不能用普通引用类型引用常量

4.const与函数

1) const类型参数

格式: image.png

void f(const int j){
    j++;            错误,表示参数i的初值在函数f( )中不能被改变。 
}
void f(const int *p){
    (*p)++;//错误
}

2)const类型返回值

可以用const修饰符修饰函数的返回值,即函数返回一个常量,此常量既可以赋给常量,也可以赋给变量。

void t1(int&){}
void t2(const int&){}
int main(){ 
    //t1(1);       //错误,标准下降,在t1()中,可以修改参数内容,而1为常量
    t2(1);           //正确,在t2()中,参数声明为常量   
    int m=10;
    const int n=20; 
    t1(m);        //正确
    t1(n);        //错误,标准下降,不能使用普通引用方式引用常量
    t2(m);        //正确,可以用常量方式引用普通变量
    t2(n);        //正确
}

例:
#include <iostream>
using namespace std;
class Tcons
{
    int iData;
    public:
    Tcons(int i = 0):iData(i)
    {
        cout<<"构造函数"<<i<<endl;
    }
    void Seti(int i)
    {
        iData = i;
        cout << "iData:" << iData <<endl;
    }
};

Tcons test1()//返回普通对象
{
    return Tcons();
}

const Tcons test2()//返回常对象
{
    return Tcons();
}

int main()
{
    test1() = Tcons(10);//正确,test1函数返回一个Tcons对象,并把对象Tcons(10)的值赋给它
    test1().Seti(20);
    test2();//正确,调用test1(),得到一个返回对象,并调用此对象的成员函数
    //test2() = Tcons(10);   //错误,常对象不能被修改
    //test2().Seti(20);      //错误,常对象内容不能被修改
    return 0;
}
运行结果:
    构造函数10
    构造函数0
    构造函数0
    iData:20
    构造函数0

先执行第一个语句右边初始化语句形参i = 20,右边Test()又调用默认的i = 0;所以第一个语句调用了两次构造函数,
第二句也是调用构造函数的默认参数,再改值;test2也调用默认值构造函数

3) const在传递地址中的应用

在函数的实参与形参结合时的传递地址过程中,对于在被调用的函数中不需要修改的指针或对象,用const修饰是合适的。

#include <iostream>
using namespace std;

void test(int *p){}            一个普通的函数test,参数为指针型

void testpointer(const int *p)
{
    //(*p)++;          //错误,不允许修改常量内容
    int i = *p;        //正确,常量赋值给变量
    //int *q = p;      //错误,标准下降,不能把一个指向常量的指针赋值给一个指向非常量的指针
}

const char *teststring()    返回值的内容为常量
{
    return "hello!";
}

const int *const testint()    返回值的内容、指针皆为常量
{
    static int i = 100;
    return &i;
}

int main()
{
    int m = 0;
    int *im = &m;
    const int *cim = &m;     //正确,可以把非常量的地址赋给一个指向常量的指针
    test(im);                  
    //test(cim);             //错误,不能把指向常对象的指针赋值给指向非常对象的指针
    testpointer(im);       
    testpointer(cim);
    //char *p = teststring();//错误,不能把指向常对象的指针赋值给指向非>常对象的指针
    const char *q = teststring();
    cout << *q <<endl;
    //int *ip = testint();   //错误,不能把指向常对象的指针赋值给指向非>常对象的指针
    const int *const ipm = testint();
    cout << *ipm <<endl;
    const int *iqm = testint();
    cout << *iqm <<endl;
    //*testint() = 10;       //错误,因为testint()返回值指向常量的指针,其内容不能被修改
    return 0;
}

5.const与类

const在类里有两种应用:
一是在类里建立类内局部常量;
二是const和类成员函数的结合使用。

1)类内const局部常量

在类里建立一个const成员时不能赋初值,只能在构造函数里对其赋初值,而且要放在构造函数特殊的地方。

class conClass{
   const int NUM;
  public:
    conClass();
};
conClass::conClass():NUM(100){}


错误示例:
class conClass{
   const int NUM=100;    //错误
   int iData[NUM];        //错误
public:
    conClass();
};

静态常量。为提高效率保证所有的类对象最多只有一份拷贝值,通常需要将常量声明为是静态的,例如:
class Student{
    static const int NUM=30;
    int iScoreList[NUM];
    …
};

2)常对象与常成员函数

常对像:
    const int i=10;
    const conClass cTest(10);
常成员函数:
    class 类名{
    ……
    返回值类型 成员函数名称(参数列表) const;
    ……
    };
    如果在函数的前面加上const,表明函数返回值为const,为了防止混淆,把const放在函数的后面。
    在一个const成员函数里,试图改变任何数据成员或调用非const成员函数,编译器都将报错。

5-5 const成员函数与非const成员函数使用方式比较
#include <iostream>
#include <string.h>
using namespace std;

class Student
{
    int no;
    char name[20];
    public:
    Student();
    int Getno()const;
    const char* Getname();
        void Print()const;
};

Student::Student()
{
    no = 1;
    strcpy(name, "wang");
}

int Student::Getno()const
{
    return no;
}

const char* Student::Getname()
{
    return name;
}

void Student::Print()const
{
  cout << "No:" << no << " Name:" << name <<endl;
}

int main()
{
    Student s1;
    s1.Getno();
    s1.Getname();
    const Student s2;
    s2.Getno();                
    //s2.Getname();  //错误,常对象调用非const成员函数
        s2.Print();                常对象只能调用常函数
    return 0;
}

如果真的想改变常对象的某些数据成员怎么办?
两种方法:
1,强制转换
2,使用mutable

#include <iostream>
using namespace std;

class Test
{
    int i,j;
    public:
    Test():i(0),j(0){}
    void f()const;
};
void Test::f()const
{
    //i = 1;                    //错误,在常成员函数中修改类成员
    ((Test*)this)->j = 5;        强制转换
    cout << "I:" << i << " J:" << j <<endl;
}

int main()
{
    const Test t;
    t.f();
    return 0;
}
#include <iostream>
using namespace std;

class Test
{
    int i;
    mutable int j;            把可能要改变的值定义的时候就定义上mutable
    public:
    Test():i(0),j(0){}
    void f()const;
};
void Test::f()const
{
    //i = 1;    //错误,在常成员函数中修改类成员
    j = 5;      
    cout << "I:" << i << " J:" << j <<endl;
}

int main()
{
    const Test t;
    t.f();
    return 0;
}

6.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,其形参是本类的对象的引用,其作用是使用一个已经存在的对象,去初始化一个新的同类对象。
格式:构造函数名(const 类名& );
在以下三种情况下会被调用:
①当用一个已经存在的对象,去初始化该类的另一个对象时
②如果函数的形参是类对象,调用函数进行形参和实参结合时
③如果函数的返回值是类对象,函数调用完成返回时
使用拷贝构造函数时注意以下问题:
(1) 并不是所有的类声明中都需要拷贝构造函数。仅当准备用传值的方式传递类对象时,才需要拷贝构造函数。
(2) 为防止一个对象不被通过传值方式传递,需要声明一个私有(private)拷贝构造函数。因为拷贝构造函数是私有的。已显式地声明接管了这项工作,所以编译器不再创建默认的拷贝构造函数。
根据情况去定义拷贝构造函数,以实现同类对象之间数据成员的传递。如果没有定义类的拷贝构造函数,系统会在必要时生成一个隐含的拷贝构造函数。 这个隐含的拷贝构造函数的功能是,把初始值对象的每个数据成员值都复制到新建的对象中

#include <iostream>
using namespace std;

class Student
{
    private:
        static int number;
    public:
        Student()
        {
            number++;
            show("Student");
        }
        Student(const Student&)                拷贝构造函数
        {
            number++;
            show("Student");
        }
        ~Student()
        {
            number--;
            show("Student");
        }
        static void show(const char* str = NULL)//指向常量的指针
        {
            if(str)
            {
                cout << str << ":";
            }
            cout << "number=" << number <<endl;
        }
};

int Student::number = 0;//静态数据成员赋值

Student f(Student x)
{
    x.show("x inside f()");
    return x;
}

int main()
{
    Student h1;
    Student h2 = f(h1);
    Student::show("after call f()");
    return 0;
}
拷贝构造函数调用三时机:
1、同类初始化 
    A c1;  A c2(c1);或A c2=c1;
    赋值不算  A c1;  A c2;   c2=c1;不会
2、作为参数传入函数
3、作为类的返回值

Student:number=1        h1构造函数
Student:number=2        h1作为参数传入调用拷贝构造函数
x inside f():number=2
Student:number=3        返回值为类对象,调用拷贝构造函数
Student:number=2        局部变量x的析构
after call f():number=2
Student:number=1        
Student:number=0

小结
本章给出了const的使用方法,以及const与指针、const与函数、const与类的各种应用。所有这些都为程序设计提供了良好的类型检查形式及安全性。
相对于指针,引用(&)是对对象的直接使用,显得更加简洁实用,为运算符重载提供了支持。
拷贝构造函数采用相同类型的对象引用作为它的参数,可以被用来从现有的类创建新类。当用传值方式传递或返回一个对象时,编译器自动调用这个拷贝构造函数。虽然,编译器将自动地创建一个拷贝构造函数,但是,如果认为需要为设计者的类创建一个拷贝构造函数,应该自己定义它,以确保正确的操作。如果不想通过传值方式传递和返回对象,应该创建一个私有拷贝构造函数(例5-11)。