Expert C++
上QQ阅读APP看书,第一时间看更新

Low-level details of objects

C++ does its best to support compatibility with the C language. While C structs are just a tool that allows us to aggregate data, C++ makes them equal to classes, allowing them to have constructors, virtual functions, inherit others structs, and so on. The only difference between a struct and a class is the default visibility modifier: public for structs and private for classes. There is usually no difference in using structs over classes or vice versa. OOP requires more than just a data aggregation. To fully understand OOP, let's find out how we would we incorporate the OOP paradigm if we have only simple structs providing data aggregation and nothing more. 

A central entity of an e-commerce marketplace such as Amazon or Alibaba is the Product, which we represent in the following way:

struct Product {
std::string name;
double price;
int rating;
bool available;
};

We will add more members to the Product if necessary. The memory layout of an object of the Product type can be pictured like this:

Declaring a Product object takes sizeof(Product) space in memory, while declaring a pointer or a reference to the object takes the space required to store the address (usually 4 or 8 bytes). See the following code block:

Product book;
Product tshirt;
Product* ptr = &book;
Product& ref = tshirt;

We can picture the preceding code as follows:

Let's start with the space the Product object takes in memory. We can calculate the size of the Product object summing up the sizes of its member variables. The size of a boolean variable is 1 byte. The exact size of the double or the int is not specified in the C++ standard. In 64-bit machines, a double variable usually takes 8 bytes and an int variable takes 4 bytes.

The implementation of std::string is not specified in the standard, so its size depends on the library implementation. string stores a pointer to a character array, but it also might store the number of allocated characters to efficiently return it when size() is called. Some implementations of std::string take 8, 24, or 32 bytes of memory, but we will stick to 24 bytes in our example. By summing it up, the size of the Product will be as follows:

24 (std::string) + 8 (double) + 4 (int) + 1 (bool) = 37 bytes.

Printing the size of the Product outputs a different value:

std::cout << sizeof(Product);

It outputs 40 instead of the calculated 37 bytes. The reason behind the redundant bytes is the padding of the struct, a technique practiced by the compiler to optimize the access to individual members of the object. The Central Processing Unit (CPU) reads the memory in fixed-size words. The size of the word is defined by the CPU (usually, it's 32 or 64 bits long). The CPU is able to access the data at once if it's starting from a word-aligned address. For example, the boolean data member of the Product requires 1 byte of memory and can be placed right after the rating member. As it turns out, the compiler aligns the data for faster access. Let's suppose the word size is 4 bytes. This means that the CPU will access a variable without redundant steps if the variable starts from an address that's divisible by 4. The compiler augments the struct earlier with additional bytes to align the members to word-boundary addresses.