最近在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 != ©Source) // 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 != ©Source) && (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(私有)而不进行实现,对于编译器通过按值传递函数或将一个实例赋值给另一个实例来拷贝该类的所有尝试,都将抛出错误。