C++入门学习笔记4:动态对象、this指针、成员指针、对象引用、常对象、类模板

By | 2020年4月2日

动态对象

new <类名>;               // 申请成功会返回一个指向对象的指针
new <类名>(<参数列表>);    // 自动调用带参构造器
delete <指向对象的指针名>;  // 手动释放对应对象的内存空间

Student *stuPtr = new Student;
delete stuPtr;

this指针

该指针是一个隐含指针——隐含在每一个成员函数中(除静态的)——每一个成员函数都有一个

其指向调用该函数的对象——即,值为当前函数所在对象的起始地址

这里有个有趣的问题——既然同一个类的所有对象都共享同一份成员函数代码,那么为什么我们调用函数的时候,函数总会找到自己属于哪个对象从而访问其该访问的成员变量呢?这就问题就是通过 this 指针解决的。

对象调用成员函数时,编译系统先将对象地址赋值给 this ,然后再调用。每次成员函数访问成员变量时,本质上都使用了 this ,只不过是没有显式地使用而已(是编译器再编译的时候把对象地址加上去的)。

this 是 const,成员函数不能重新赋值之。

静态成员不能访问 this(显然,static成员 不属于任何的 that )

this 的显式使用

this 指针,返回的是指向当前对象自身的指针。对象用自己的函数返回自己的成员,可以:

this->memberVariable;   // 是指针
(*this).memberVariable; // 是对象

// 用 this 来进行拷贝
void Student::copy(const Student & stu) {
    if( this != &stu ) {    // 检查一下当前在哪个对象里,避免自己拷自己
        ...
    }
}

成员指针

可以使用一个指针绕过对象本身,直接访问该对象的成员。

实现这样功能的指针就叫做成员指针。

指向非静态数据成员的指针

定义方法与普通指针完全相同。

int *p = &time.hour;

指向非静态数据成员函数的指针

普通的指向普通函数的指针:

void (*p)();    // 普通的指向 void 型函数的指针变量
p = func;       // func 是一个定义好的函数,这一行用指针指向它
(*p)();         // 通过指针变量调用,等效于 func();

指向public函数的指针:

函数类型名 (类名::*指针变量名)(参数表);    // 要表明指针指向哪个类中的成员函数
指针变量名 = &类名::成员函数名;           // 指向类中的函数

int (Point::*pGetX)(int a);     // 比较一般的写法

void (*p)();
p = &Time::showTime;
(t.*p)();               // 新的调用方法,等效于 t.showTime();

指向静态成员的指针

class Point {
    ...
    static int count;
    ...
    static void GetC() {
        ...
    }
};
int Point::count = 123;             // 静态数据成员,类外初始化

int *countPtr = &Point::count;      // 指上静态数据成员
void (*gc)() = Point::GetC;         // 指上静态成员函数
gc();               // 调用就这样

对象引用

Reference,是某个变量的别名 alias

引用,是直接访问对象。指针,是间接访问

Time myTime;
Time &refTime = myTime;

refTIme.func();
myTime.func();      // 完全等效

引用调用

Student returnS(Student s) {return s;}
Student stu1;
stu1.returnS(stu1); // 到这一行,首先会调用 Student 类的复制构造器。
                    // 复制构造器将形参 s 初始化为实参 stu1
                    // 然后,第二次构造复制构造器,以将 return s 的这个返回值对象初始化为 s
                    // 接下来 returnS 的返回值对象调用析构器,将返回值对象析构。
                    // 然后形参 s 对象的析构器也要被调用。
                    // 就这样,啥都没干就调用了两次构造器、调用了两次析构器。成本过高。

而,参数的引用传递可以有效避免值传递带来的高额开销。

Student& returnS(const Student& s) {
    return s;
}

这样会直接将引用传递过去,没有任何产生副本造成的额外的开销。

共享数据的保护——常对象

对于需要被共享,且值不能被改变的量,可以设置为常量。

const <数据类型名> <常量名>=<表达式>;

常对象:其数据成员值在该对象生命周期内不能被改变

const <类名> <对象名>(<初始化值>);       // 常用这种
<类名> const <对象名>(<初始化值>);       // 两种效果相同

const Time t(1, 1, 1);      // 其所有的数据都是常量,因此必须被初始化

常对象不能调用普通的成员函数——防止在成员函数中尝试修改常对象数据的值

编译时,编译以一个源程序文件作为单位来进行编译。如果函数的定义和声明、调用不在一起,那么编译器就无法对函数内部结构进行检查,导致错误遗留到链接、运行阶段。因此,编译器干脆就不对函数内部进行检查。

mutable

常对象中, 用 mutable 声明的变量,可以被声明为 const 的成员函数修改。

类的常成员

加 const 声明的变量和函数。

const 变量 只能 通过构造器的参数初始化列表来对常成员进行初始化。

const int Hour;
Time::Time(int h): Hour(h) {}

常成员函数

可以通过常成员函数访问数据成员,但不能够修改值,也不能调用该类的非 const 函数

<数据类型> <函数名> (<参数表>) const;     // 这个 const 要写在最后。声明和定义时候都要加

常对象只能调用常成员函数

常对象中的函数 不等于 常成员函数!只有带 const 的才是常函数!

const 还可以用于对重载函数的区分。

class R {
    R(int i, int j) {
        R1 = i;
        R2 = j;
    }
    void print();
    void print() const;

    int R1, R2;
};

...
R a(5, 4);
a.print();  // 普通对象,调重载的普通函数

const R b(1, 2);    // 声明一个常对象
b.print();  // 常量对象,调重载的常函数

const 指针

指向对象的常指针

这样的指针指向不能再改变,但其所指的对象可以改变。

类名* const 指针变量名 = 对象地址;
指向常对象的指针

常对象只能用 const 型的指针指向。

const 类型名* 指针变量名;

这样声明的指针可以指向常对象,也可以指向普通对象。不能通过指针改变对象的值,但是指针本身的值可以改变(也就是重新指向其他对象)。

这也就是说,可以实现【有保护的使用】

例:

Time t1, t2;
const Time* p=&t1;
(*p).hour=18;   // 错,不能通过常指针修改变量
t1.hour = 18;   // 对,t1 不是常对象
p = &t2;        // 对,指向常对象的指针仍然是指针,可以被重新赋值

常引用

声明引用时使用 const 修饰的引用

常引用所引用的对象不能被更新。

const 数据类型 &引用名

常引用参数:函数中不能改变实参对象的值

void fun(const Time &t);

对象数组

<类名> <数组名>[<下标表达式>];
// 数组建立时,每个元素都是一个独立的对象,也就是说有多少个元素就要创建多少次对象
Student stud[3] = [11, 22, 33]; // 这样填写构造器实参
Student exStud[2] = {Student(11, 'abc'), Student(22, 'def')};
                        // 构造器有多个参数,需要这样调用构造器
Student exStud[2] = {Student(11, 'abc')};
                        // 如果构造器有默认参数值,则第二个直接通过默认参数构造

访问:

<数组名>[<下标>].<成员名>

动态对象数组

CPoint* ptr = new CPoint[5];    // 声明并分配空间
...
delete[] ptr;   // 释放上面动态申请的内存

对象成员(子对象)

class A {
    ...
};
class B {
    B(const A &a):m_a(a) {  // 在这里调用了复制构造器,把 a 复制给 m_a
        ...
    }
    /* 或
        B(const A &a):m_a(a) {
            m_a = a;
            ...
        }
    */

    A m_a;      // 对象成员。A需要在B前面进行声明
};

有子对象的类在进行初始化时,先调用子对象的构造器,再调用本类的构造器。析构相反:即先析构自己,再析构各个子对象。

如果类没有写自定义构造器,则子对象构造时使用的也是默认构造器。

与对象成员类似,还有一种叫做对象成员数组的东西

类模板

类模板是类的抽象,类是类模板的实例。

类模板,是对一批【仅有数据成员类型不同的】类的抽象。

因为在这个类中,数据类型也成为了参数,故又称“参数化的类”。

类模板实例化出来的类:“模板类”(从模板来的类)

template <class 类型参数>   // 类型参数数量不限
class <类模板名> {
    ...
};

// 例1:
template <class T>
class Compare {
public:
    Compare() {
        x=0;
        y=0;
    }
    Compare(T a, T b) {
        x=a;
        y=b;
    }
    T max() {
        return (x>y)?x:y;
    }
private:
    T x, y;
};

// 例2:
template <class T1, class T2>       // 多个类型的参数
class A {
    ...
};

A<int, double> obj;  // 实例化对象。先实例化出对应类型的类,再实例化对象


template <class T=int>      // 使用默认参数的类模板
class Array {
    ...
};
Array<> intArray;       // 可以留空

点击量:278

发表评论

邮箱地址不会被公开。 必填项已用*标注