实现OpenGL渲染器语法篇(一)——size_t,this指针,初始化列表以及操作符重载

最近在Github上复现了一个渲染器render的项目:
Github链接:tiny render

我希望在博客上可以记录自己的学习过程,博客主要分为两大类:《原理篇》和《语法篇》。
原理篇则主要讲的是——实现渲染器过程中所需要的图形学渲染算法知识。
语法篇则主要讲的是——实现渲染器过程中使用到的C++的语法知识。


一、size_t和this指针

size_t的语法——size_t是一种无符号的整数,扩展size_t

this指针的用法:

this是一个在类范围内适用的保留关键字,它包含对象的地址。

换句话说,this的值就是&object。

在类的一个成员方法中,当你调用另一个成员方法时,编译器会发送这个指针,作为函数调用中的隐式、不可见的参数:

class Human
{
private:
    void Talk (string Statement)
    {
        cout << Statement;
    }
    
public:
    void IntroduceSelf ()
    {
        Talk("Bla bla");
        // same as Talk(this, "Bla Bla")
    }
};

大家都看到了,IntroduceSelf()使用了private的成员函数Talk()去向屏幕上打印一个statement。

事实上,在调用Talk()的时候,编译器内嵌了this指针,它被调用为Talk(this, Bla Bla)。


从一个编程的角度来说,this并没有太多的应用,除了那些通常是可选的。

举个栗子,看代码:

#include <iostream>
using namespace std;

class Human
{
private:
    // Private member data
    int age;
    
public:
    void SetAge (int inputAge)
    {
        age = inputAge;
    }
    
    // Human lies about his or her age (if over 30)
    int GetAge()
    {
        if (age > 30)
            return (age - 2);
        else
            return age;
    }
};

int main()
{
    Human firstMan;
    firstMan.SetAge(35);
    
    Human firstWoman;
    firstWoman.SetAge(22);
    
    cout << " Age of firstMan " << firstMan.GetAge() << endl;
    cout << " Age of firstWoman " << firstWoman.GetAge() << endl;
    
    return 0;
}

上面这段代码中,在SetAge()中访问age,可以有一个变形:

void SetAge(int humansAge)
{
    this->age = humansAge;      // same as age = humansAge
}

注意,this指针不会发送到声明为static的类方法,因为静态函数未连接到该类的实例。相反,它们被所有实例共享

要在静态函数中使用实例变量,你需要显式声明一个参数并将this指针作为参数发送。

最后,那么使用this指针的目的是什么?大家可以看看这个链接


二、构造函数的初始化列表和重载

2.1 具有初始化列表的构造函数

大家都知道构造函数在初始化成员变量时是很有用的。另一个初始化成员的方法是使用初始化列表

我们来看看两种ctor的方法的对比。

// A Class with Overloaded Constructor(s) and No Default Constructor
// 具有重载构造函数且没有默认构造函数的类
// LISTING 9.5
// p229

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
    string name;
    int age;
public:
    Human (tring humansName, int hunmansAge)
    {
        name = humansName;
        age  = humansAge;
        
        cout << "Overloaded ctor creates " << name;
        cout << " of age " << age << endl;
    }
    
    void IntroduceSelf ()
    {
        cout << "I am " + name << " and am ";
        cout << age << " years old" << endl;
    }
};

int main()
{
    Human firstMan("Adam", 25);
    Human firstWoman("Eve", 28);
    
    firstMan.IntroduceSelf();
    firstWoman.IntroduceSelf();
}

第二种的写法其实上面这第一个的变形:

class Human
{
private:
    string name;
    int age;
public:
    // two params to initialize members age and name
    // 初始化成员年龄和名称的两个参数
    Human (string humansName, int humansAge)
        : name(humansName), age(humansAge)
        {
            cout << "Constructed a human called " << name;
            cout << ", " << age << " years old" << endl;
        }
};

int main()
{
    Human adam;
    Human eve("Eve", 18);
    
    return 0;
}

那么像我们这次的tgaimage.cpp中,应用到这里的是——

TGAImage::TGAImage(int w, int h, int bpp) : data(NULL), width(w), height(h), bytespp(bpp)
{
    // data、width、height、bytespp都是TGAImage类中定义的private成员
    // ()中的值就是初始化的值
    // 也就是说 data = NULL,width = w 等等
    unsigned long nbytes = width * height * bytespp;
    data = new unsigned char[nbytes];
    memset(data, 0, nbytes);
}

2.2 构造函数重载

构造函数可以像函数一样重载。因此,我们可以编写一个构造函数ctor,这个ctor要求使用name作为param来实例化Human,例如:

class Human
{
public:
    Human()
    {
        // default constructor code here
    }
    
    Human(string humansName)
    {
        // overloaded constructor code here
    }
};

下面的code即将演示了重载构造函数的应用,它使用构造时提供的name创建了类Human的一个对象object。

//  --------A Class Human with Multiple Constructors----------
#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
    string name;
    int age;
    
public:
    Human()  // default ctor
    {
        age = 0; // 初始化从而确保没有垃圾值
        cout << "默认构造函数:未设置名称和年龄" << endl;
    }
    
    Human(string humansName, int humansAge)  //overloaded
    {
        name = humansName;
        age  = humansAge;
        
        cout << "Overloaded constructor creates ";
        cout << name << " of " << age << " years" << endl;
    }
};

int main()
{
    Human firstMan; // use default constructor
    Human firstWoman ("Eve", 20); // use overloaded constructor
}

在tgaimage.cpp中,使用到的ctor重载是下面这样——

TGAImage::TGAImage() : data(NULL), width(0), height(0), bytespp(0)
{
}

TGAImage::TGAImage(const TGAImage& img) : data(NULL), width(img.width), height(img.height), bytespp(img.bytespp)
{
    unsigned long nbytes = width * height * bytespp;
    data = new unsigned char[nbytes];
    memcpy(data, img.data, nbytes);
}

三、操作符重载

3.1 编程一个一元递增递减运算符

可以在类声明中使用以下语法对一元前缀递增运算符(++)进行编程:

// Unary increment operator (prefix)
Date& operator ++ ()
{
    // operator implementation code
    return *this;
}

另一方面,后缀递增运算符(++)具有不同的返回类型和输入参数(并不总是使用):

Date operator ++ (int)
{
    // Store a copy of the current state of the object, before incrementing day
    Date copy (*this);
    // increment implementation code
    // Return state before increment (because, postfix)
    return copy;
}

前缀和后缀递减运算符与递增运算符具有类似的语法,只是声明中会包含一个 - - 就是 + + 出现的位置。下面的code中显示了一个简单的Date类,该类允许使用运算符(++)来增加天数。

// LISTING 12.1 
// A Calendar Class That Handles Day, Month, and Year, and Allows
// Incrementing and Decrementing Days

#include <iostream>
using namespace std;

class Date
{
private:
    int day;
    int month;
    int year;

public:
    Date (int inMonth, int inDay, int inYear)
        : month (inMonth), day(inDay), year (inYear) {};
    
    Date& operator ++ () // prefix increment
    {
        ++day;
        return *this;
    }
    
    Date& operator -- () // prefix decrement
    {
        --day;
        return *this;
    }
    
    void DisplayDate()
    {
        cout << month << " / " << day << " / " << year << endl;
    }
};

int main ()
{
    Date holiday (12, 25, 2016); // Dec 25, 2016
    cout << "The date object is initialized to: ";
    holiday.DisplayDate ();
    
    ++holiday; // move date ahead by a day
    cout << "Date after prefix-increment is: ";
    holiday.DisplayDate ();
    
    --holiday; // move date backwards by a day
    cout << "Date after a prefix-decrement is: ";
    holiday.DisplayDate ();
    
    return 0;
}

3.2 编程二元运算符加法(a+b)和减法(a-b)运算符

与递增递减运算符类似,定义了二进制加减运算符后,可以从实现这些运算符的类的对象中添加或减去受支持的数据类型的值。

再看看之前定义的calendar Date类。虽然您已经实现了增加日期的功能,以便将日历向前移动一天,但是您仍然不支持将日历向前移动5天。因此,需要实现二元操作符(+),如下面的代码所示。

// LISTING 12.4 Calendar Class Featuring the Binary Addition Operator
// 具有二元加法运算符的日历类

#include <iostream>
using namespace std;

class Date
{
private:
    int day, month, year;
    string dateInString;

public:
    Date(int inMonth, int inDay, int inYear)
        : month(inMonth), day(inDay), year(inYear) {};
    
    Date operator + (int daysToAdd) // binary addition
    {
        Date newDate (month, day + daysToAdd, year);
        return newDate;
    }
    
    Date operator - (int daysToSub) // binary subtraction
    {
        return Date(month, day - daysToSub, year);
    }
    
    void DisplayDate()
    {
        cout << month << " / " << day << " / " << year << endl;
    }
};

int main()
{
    Date Holiday (12, 25, 2016);
    cout << "Holiday on: ";
    Holiday.DisplayDate ();
    
    Date PreviousHoliday (Holiday - 19);
    cout << "Previous holiday on: ";
    PreviousHoliday.DisplayDate();
    
    Date NextHoliday(Holiday + 6);
    cout << "Next holiday on: ";
    NextHoliday.DisplayDate ();
    
    return 0;
}

3.3 实现加法赋值(+=)和减法赋值(-=)运算符

加法赋值运算符允许a += b这样的语法;这允许程序员将对象a的值增加一个数量b。在此过程中,加法赋值运算符的作用是可以重载,从而它来接受不同类型的参数b。接下来的code允许你向Date对象添加一个整数值。

// LISTING 12.5 Defining Operator (+=) and Operator (-=) to Add or Subtract Days
// in the Calendar Given an Integer Input

#include <iostream>
using namespace std;

class Date
{
private:
    int day, month, year;
    
public:
    Date(int inMonth, int inDay, int inYear)
        : month(inMonth), day(inDay), year(inYear) {}
    
    void operator+= (int daysToAdd) // addition assignment
    {
        day += daysToAdd;
    }
    
    void operator-= (int daysToSub) // subtraction assignment
    {
        day -= daysToSub;
    }
    
    void DisplayDate()
    {
        cout << month << " / " << day << " / " << year << endl;
    }
};

int main()
{
    Date holiday (12, 25, 2016);
    cout << "holiday is on: ";
    holiday.DisplayDate ();
    
    cout << "holiday -= 19 gives: ";
    holiday -= 19;
    holiday.DisplayDate();
    
    cout << "holiday += 25 gives: ";
    holiday += 25;
    holiday.DisplayDate ();
    
    return 0;
}

3.4 重载复制赋值操作符(=)

有时需要将一个类中实例的内容分配给另一个实例,如下所示:

Date holiday(12, 25, 2016);
Date anotherHoliday(1, 1, 2017);
anotherHoliday = holiday; // uses copy assignment operator

此赋值调用默认的复制赋值操作符,当你没有提供该操作符时,编译器已将其内置到你的class中。

根据类的性质,默认的复制分配操作符可能不够用,特别是如果你的class正在管理一个不会被复制的资源默认复制赋值操作符的问题与默认复制构造函数的问题类似。

为了确保深度复制,与拷贝构造函数一样,大家需要指定一个附带的拷贝赋值操作符

ClassType& operator= (const ClassType& copySource)
{
    if(this != &copySource) // protection against copy into self
                          // 防止复制进入自我
    {
        // copy assignment operator implementation
        // 拷贝赋值操作符的实现
    }
    return *this;
}   

如果大家定义的类封装了一个原始指针,如清单9.9所示的MyString类,那么深度复制是很重要的。

// LISTING 9.9 Define a Copy Constructor to Ensure Deep Copy of Dynamically Allocated
// Buffers

#include <iostream>
#include <string.h>
using namespace std;
class MyString
{
private:
    char* buffer;
public:
    MyString(const char* initString) // constructor
    {
        buffer = NULL;
        cout << "Default constructor: creating new MyString" << endl;
        if(initString != NULL)
        {
            buffer = new char [strlen(initString) + 1];
            strcpy(buffer, initString);
            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }
    
    MyString(const MyString& copySource) // Copy constructor
    {
        buffer = NULL;
        cout << "Copy constructor: copying from MyString" << endl;
        if(copySource.buffer != NULL)
        {
            // allocate own buffer
            buffer = new char [strlen(copySource.buffer) + 1];
            // deep copy from the source into local buffer
            strcpy(buffer, copySource.buffer);
            
            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }

// Destructor
    ~MyString()
    {
        cout << "Invoking destructor, clearing up" << endl;
        delete [] buffer;
    }
    
    int GetLength()
    { 
        return strlen(buffer); 
    }
    
    const char* GetString()
    { 
        return buffer; 
    }
};

void UseMyString(MyString str)
{
    cout << "String buffer in MyString is " << str.GetLength();
    cout << " characters long" << endl;
    
    cout << "buffer contains: " << str.GetString() << endl;
    return;
}

int main()
{
    MyString sayHello("Hello from String Class");
    UseMyString(sayHello);
    
    return 0;
}

为了在分配期间确保深度复制,定义一个拷贝分配操作符,如Listing 12.8所示。

// LISTING 12.8 A Better class MyString from Listing 9.9 with a Copy Assignment
// Operator =

#include <iostream>
using namespace std;
#include <string.h>

class MyString
{
private:
    char* buffer;
public:
    MyString(const char* initialInput)
    {
        if(initialInput != NULL)
        {
            buffer = new char [strlen(initialInput) + 1];
            strcpy(buffer, initialInput);
        }
        else
               buffer = NULL;
    }
    
    // Copy assignment operator
    MyString& operator= (const MyString& copySource)
    {
        if ((this != &copySource) && (copySource.buffer != NULL))
        {
            if (buffer != NULL)
                delete[] buffer;
            // ensure deep copy by first allocating own buffer
            buffer = new char [strlen(copySource.buffer) + 1];
            // copy from the source into local buffer
            strcpy(buffer, copySource.buffer);
        }
        return *this;
    }
    
    operator const char*()
    {
        return buffer;
    }
    
    ~MyString()
    {
        delete[] buffer;
    }
};

int main()
{
    MyString string1("Hello ");
    MyString string2(" World");
    
    cout << "Before assignment: " << endl;
    cout << string1 << string2 << endl;
    string2 = string1;
    cout << "After assignment string2 = string1: " << endl;
    cout << string1 << string2 << endl;
    
    return 0;
}

行24~37是copy assignment operator的核心代码,它在功能上类似于拷贝构造函数,并且执行启动检查(check)以确保同一对象不是复制源和目标

检查(check)返回true之后,MyString的拷贝赋值操作符在从复制源重新分配文本空间之前,首先释放其内部缓冲区(buffer),然后,使用strcpy()进行复制,如第18行所示。

NOTE:

清单12.8与清单9.9相比,清单12.8中的另一个细微变化是将函数GetString()替换为 operator const char*,如第39至42行所示。这个操作符让 类MyString 的使用更加简单,如第56行所示,其中使用一个cout语句来显示MyString的两个实例。

CAUTION:

在实现管理动态分配资源(如使用new分配的数组)的类时,始终要确保除了构造函数和析构函数外,还实现了(或评估了)拷贝构造函数和拷贝赋值操作符。

除非在复制类的对象时处理资源所有权问题,否则类是不完整的,并且会危及应用程序的稳定性。

TIP:

若要创建无法复制的类,请将拷贝构造函数和拷贝赋值操作符声明为private(私有)。

声明为private(私有)而不进行实现,对于编译器通过按值传递函数或将一个实例赋值给另一个实例来拷贝该类的所有尝试,都将抛出错误。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容