2016.12.10《C++游戏编程入门》之感悟

《2016.12.10《C++游戏编程入门》之感悟》

与其说这本书教用C++做游戏,不如说借助一些控制台程序教C++入门知识。。。所以如果读者不是C++入门人士,我不推荐这本书。但是对我来说,还是不错的,就当做是复习吧。

之前总是不明白为什么说尽量给函数传递引用而非值传递;
这几天看了一本叫《C++游戏编程入门》的书之后,看到了答案,并且加深了我自己的认识:

传递变量时,函数获得变量的副本,即不能修改传递的原始变量(实参变量)。或许有时候正好需要这样。。。因为这样可以保证实参的安全,使其无法被修改。但反之,,在其他时候,也许希望从接受实参的函数内部对实参进行修改。

使用引用正好可以解决这一点;

一、按值传递和按引用传递:

请看下面的例子:

#include<iostream>
using namespace std;

void badSwap(int x, int y);
void goodSwap(int &x, int &y);

int main()
{
    int myScore = 150;
    int yourScore = 1000;
    //调用按值传递:
    badSwap(myScore,yourScore);

    //调用按引用传递:
    goodSwap(myScore,yourScore);
}

void badSwap(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}

void goodSwap(int& x, int& y)
{
    int temp = x;
    x = y;
    y = temp;
}

在这个例子中,我们先调用了按值传递的函数,让我们看看发生了什么:

当程序运行到badSwap(int x, int y);这句话的时候,形参x和y将会获得实参的一份副本,而不是访问实参的自身。即myScore和yourScroe的副本传送给了形参x和y:badSwap(myScore, yourScore);

当badSwap( )执行其核心部分时,x和y确实交换了值:x变成了1000,y变成了150。然而,当函数结束时,x和y都超出作用并且不复存在。程序控制权随后返回到main( )函数,其中myScore和yourScore没有改变。

再看下面调用的按引用传递函数:

通过将指向实参的引用传递给形参可以让函数拥有实参的访问权。因此,任何对形参的操作都会对实参起作用。当程序运行到goodSwap(int& x, int& y);
这句话的时候,形参x会引用myScore,而形参y会引用yourScore:
goodSwap(myScore, yourScore);

x只是myScore的另一个名称,y是yourScore的另一个名称。当goodSwap( )执行且x和y交换值的时候,真正发生的是myScore和yourScore交换了值。

二、按引用传递提高效率

通过值传递变量会造成一些额外的开销,因为在把变量赋值给形参之前必须对其进行复制。在谈论如int或float这样内置类型的变量时,这种开销可以忽略不计。但是对于像表示整个3D世界的大型对象来说,复制的开销就很大了。这个时候通过引用传递参数则效率较高。因为这样不需要对实参进行复制,而只是通过引用向函数提供已存在对象的访问权

三、当我们不希望传入的参数被修改怎么办?

问题来了,例子里面按引用传递,会被修改;那么有时候不希望被修改怎么办呢?

在传入引用的前面加上关键字const就行了:

void display(const vector<string>& vec)

添加const,告诉编译器,让编译器进行监督;如果程序有对引用进行修改,那么就会报错。此时的形参更像一个只读的参数;

四、返回引用:

既然一个函数(方法)可以接收引用,那么它也可以返回引用;如果函数(方法)返回的是一个较大的对象,这个操作的开销会很大,这时返回引用就是高效的选择。

值得注意的是,光从函数中的最后的return语句,是看不出来返回的引用的,例如这个函数体中:

string& refToElement(vector<string>& vec, int i)
{
    return vec[i];
}

只有从函数头部和函数原型决定函数返回的是对象还是对象的引用。

五、返回引用的陷阱:

必须小心,返回的引用不能指向超出作用域范围的对象,因为这些对象不复存在,例如:

string& BadrefToElement()
{
    string Local = "This string will cease to exist once the function ends.";
    return Local;
}

六、返回引用之后?

函数(方法)返回引用之后,可以直接用cout显示在屏幕上;
还可以直接赋值给新的引用;
可以赋值给一个变量,但是值得注意的是:

string str = refToElement(inventory,2)

这段代码并没有将引用赋值给str,它无法做到这一点,因为str是一个string对象,代码所做的是将返回的引用所指向的元素进行复制,然后将该string对象的新的副本赋值给str。因为这种赋值涉及对象的复制,所以它比将引用赋值给引用的开销更大。

七、和指针对比一下?

看了这本书后面的内容,继续回来完善一下这篇文章;
讲了引用之后,继而又讲到了指针;其实有一个概念和引用是一模一样的,那就是常量指针:

int score = 100;
int* const pScore = &score;

常量指针感觉就像是一个固定的标杆,永远指在某个位置不会变了;跟引用差不多呢。。。绑定在了一起。
常量指针必须先初始化!!这和引用类似,引用也必须初始化;

我在编译器中尝试,如果不初始化常量指针:

《2016.12.10《C++游戏编程入门》之感悟》

实际上,建议每个指针在创建的时候都初始化;
继上面的那个传引用修改,交换两个值的例子,这次,我改为传入两个常量指针,也能达到一样的效果:

#include<iostream>
using namespace std;

void goodSwap(int &x, int &y);

int main()
{
    int myScore = 150;
    int yourScore = 1000;
    
    //调用指针传递:
    goodSwap(&myScore,&yourScore);
}

void goodSwap(int* const pX, int* const pY)
{
    int temp = *pX;
    *pX = *pY;
    *pY = temp; 
}

在传递指针时,实际传递的只是对象的地址。这样效率极高,尤其是在传递的对象占据较大内存块的时候。传递指针如同用电子邮件给某个好友发送某个网站的URL,而不是将整个网站发过去。

将形参设置为设置为常量指针是因为尽管要对它们指向的值进行修改,但不需要修改指针自身。这正是引用的工作方式,即可以修改引用指向的值,而不是修改它们自身。

八、返回指针

尽管返回指针是一种向调用函数返回信息的有效方式,但必须谨防返回指向超出作用域范围的对象的指针;不要返回指向局部变量的指针!!

九、动态分配内存

运用new操作符,在堆中分配内存,然后返回其地址;

int* pHeap = new int;

局部指针pHeap指向堆中新分配的内存块。注意,pHeap所指向的内存块在堆中,而不是在栈中。

堆中内存的主要优点之一是,它能在其分配时所在的函数之外继续存活。这意味着可以在函数中创建一个堆中的对象,然后返回它的指针或者引用;(这句话很关键!)
例子:

int* pHeap2 = intOnHeap();

int* intOnHeap()
{
    int* pTemp = new int(20);
    return pTemp;
}

画个图来解释一下:

《2016.12.10《C++游戏编程入门》之感悟》

值得一提的是,pTemp本身是栈里面的变量,指向的才是堆里面的东西;当函数结束的时候,pTemp本身会被回收(销毁);

到目前为止,如果要返回函数中创建的值,必须返回其副本。但是通过使用动态内存,可以在函数中将对象创建在堆中,然后返回新对象的指针。

到函数结束后,pTemp不复存在了;现在内存块的控制权掌握在pHeap2的手上了!最后需要delete;直接delete pHeap2就行了。

关于内存泄露:

介绍两种内存泄露的情况:

void leak1()
{
      int* drip1 = new int(30);
}

正如前面所说,drip1在函数结束后,会被销毁,堆中的内存无法控制;

《2016.12.10《C++游戏编程入门》之感悟》

void leak2()
{
    int* drip2 = new int(50);
    drip2 = new int(100);
    delete drip2;
}

《2016.12.10《C++游戏编程入门》之感悟》

十、继承与多态

继承的概念已经不必再多提;但是值得注意的是:
如果前提条件是把基类完全公开继承(class derive : public base),但是基类的一些成员函数并没有被派生类继承,它们是:

构造函数
拷贝构造函数
析构函数
重载的赋值运算符

在派生类中必须自己编写这些函数!

下面这些知识,是我之前上课并没听到或者有太留意的:

1.调用与重写基类的成员函数:

调用基类的构造函数:我们知道,当实例化派生类的时候,会自动调用基类的构造函数;但是我们也可以从派生类的构造函数中显式地调用基类构造函数;

2.重写虚基类成员函数:

值得注意的是:在重写一个基类的重载成员函数时,基类成员函数的所有重载版本都会被覆盖掉,意味着访问其他版本的成员函数的唯一方式是显示地调用基类成员函数。因此,如果要重写重载成员函数,最好重写重载函数的每个版本。

3.调用基类成员函数:

重写基类方法,然后在派生类的新定义中显式地调用该基类成员函数,并添加一些新功能,这样可以在派生类中扩展基类成员函数的工作方式。

4.在派生类中使用重载赋值运算符与拷贝构造函数:

因为之前说了,它们不会被继承过来,因此解决办法是显式地调用基类中的这两个函数。

#include <iostream>
using namespace std;

class Enemy
{
public:
    Enemy(int damage = 10);
    void virtual Taunt() const;     //made virtual to be overridden
    void virtual Attack() const;    //made virtual to be overridden
 
private:
    int m_Damage;
};

Enemy::Enemy(int damage):                //构造函数:初始化列表
    m_Damage(damage)
{}

void Enemy::Taunt() const
{
    cout << "The enemy says he will fight you.\n";
}  

void Enemy::Attack() const
{
    cout << "Attack! Inflicts " << m_Damage << " damage points.";
}

class Boss : public Enemy
{
public:
    Boss(int damage = 30);
    void virtual Taunt() const;      //optional use of keyword virtual
    void virtual Attack() const;     //optional use of keyword virtual
};

Boss::Boss(int damage): 
    Enemy(damage)            //显式调用基类的构造函数!!!
{}

void Boss::Taunt() const     //重写虚基类函数!!!
{
    cout << "The boss says he will end your pitiful existence.\n";
}  

void Boss::Attack() const    //override base class member function
{
    Enemy::Attack();         //调用基类成员函数!!! 
    cout << " And laughs heartily at you.\n";
}   

当程序实例化一个boss的对象时, 先调用了Enemy的构造函数,传递值30,并将其赋值给了m_Damage; 然后再执行Boss的构造函数(它没有执行任何操作),最后该对象就完成了。

    原文作者:Yuanyz
    原文地址: https://www.jianshu.com/p/0410e4d9d2c6
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞