[C++ Chapter] Setting Sail—An Introduction to C++ (Part 2)

Time: Column:Mobile & Frontend views:245

I. References

1. Concept of References

A reference in C++ is a type that provides an alias for a variable. A reference is not an independent data type but another view of an existing variable. References are declared using the & symbol.

A reference does not define a new variable but instead acts as an alias for an existing variable. The compiler does not allocate memory for a reference; it shares the same memory space as the variable it references. For example, in the novel Water Margin, Li Kui is also known as "Iron Ox" and "Black Whirlwind"; similarly, Lin Chong is nicknamed "Panther Head."


2. Basic Syntax of References

int a = 10;       // Define an integer variable
int &b = a;       // b is a reference to a

In this example, b is a reference to a. Modifying the value of b actually changes the value of a.


3. Characteristics of References

  1. Alias: A reference is an alias for a variable. Any operation on the reference is effectively an operation on the original variable.

  2. No Additional Memory Usage: A reference does not occupy additional memory space; it is merely another identifier pointing to the same memory address.

  3. Must Be Initialized: A reference must be initialized when it is created and cannot be reassigned to refer to another variable.

  4. Cannot Be Null: A reference cannot be assigned to nullptr; it must reference a valid object.


3.1 Alias

A reference is an alias for a variable. This means any operation on the reference directly affects the original variable. References do not have independent memory space; they simply provide a new name for the original variable.

Example:

#include <iostream>

int main() {
    int a = 42;       // Define an integer variable `a`
    int &b = a;       // `b` is a reference to `a`

    std::cout << "a: " << a << ", b: " << b << std::endl; // Outputs: a: 42, b: 42

    b = 100;          // Modify the value of `a` through reference `b`
    std::cout << "After changing b..." << std::endl;
    std::cout << "a: " << a << ", b: " << b << std::endl; // Outputs: a: 100, b: 100

    return 0;
}

In this example, modifying b directly affects a and vice versa.


3.2 No Additional Memory Usage

A reference does not allocate additional memory; it simply points to the memory address of an existing variable.

Example:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

int main() {
    int a = 0;
    // References: b and c are aliases for a
    int &b = a;
    int &c = a;
    // Create an alias for b, which still aliases a
    int &d = b;
    ++d;

    // Check memory addresses to see they are identical
    cout << "a: " << &a << endl;
    cout << "b: " << &b << endl;
    cout << "c: " << &c << endl;
    cout << "d: " << &d << endl;
    return 0;
}

Here, the addresses of a, b, c, and d are identical, proving that references do not consume additional memory.


3.3 Must Be Initialized

A reference must be initialized when declared. It cannot be assigned a value or point to another variable after initialization, ensuring safety.

Example:

#include <iostream>

int main() {
    int a = 5;

    // int &b; // Error: Reference must be initialized
    int &b = a; // Correct: `b` is initialized as a reference to `a`

    std::cout << "a: " << a << ", b: " << b << std::endl; // Outputs: a: 5, b: 5

    return 0;
}

Attempting to declare an uninitialized reference results in a compilation error.


3.4 Cannot Be Null

A reference cannot be set to nullptr; it must always reference a valid object. This ensures that a reference always points to a valid memory address.

Example:

#include <iostream>

int main() {
    int a = 10;
    int &b = a; // Correct: `b` references `a`

    // int &c = nullptr; // Error: References cannot be null

    std::cout << "b: " << b << std::endl; // Outputs: b: 10

    return 0;
}

Attempting to assign a nullptr to a reference causes a compilation error, ensuring that references are always valid.


4. Use Cases of References

4.1 Function Parameter Passing

Using references as function parameters avoids copying large objects, saving memory and time. It allows direct modification of the original data without creating a duplicate.

Example:

#include <iostream>

void increment(int &num) {
    num += 1; // Directly modify the original data
}

int main() {
    int value = 5;
    increment(value); // Pass the reference of `value`
    std::cout << "Incremented value: " << value << std::endl; // Outputs: 6
    return 0;
}

Here, the increment function modifies value directly via its reference.


4.2 Return Values

C++ functions can return references, allowing modification of original data outside the function. However, caution is required, especially to avoid returning references to local variables.

Example:

#include <iostream>

int& getReference(int &x) {
    return x; // Return reference to `x`
}

int main() {
    int a = 10;
    getReference(a) = 20; // Directly modify `a`
    std::cout << "Updated value: " << a << std::endl; // Outputs: 20
    return 0;
}

4.3 Constant References

A constant reference (const) allows read-only access to a variable via its reference. This is useful when you want to protect data from being modified while avoiding the cost of copying large objects.

Example:

#include <iostream>

void printValue(const int &num) {
    std::cout << "Value: " << num << std::endl; // Read-only operation
}

int main() {
    int a = 10;
    printValue(a);  // Outputs: 10
    printValue(20); // Outputs: 20
    return 0;
}

In this example, printValue uses const int &num to access data safely and efficiently.


5. Relationship Between References and Pointers

References and pointers are two essential concepts in C++ that allow indirect access to variables. However, they differ significantly in syntax, functionality, and usage. Below is a comparison of the two.

(1) Basic Definition

Reference:
A reference is an alias for a variable, pointing to an existing variable and requiring initialization at the time of creation. A reference does not occupy additional memory space; it is simply another name for the original variable.

Pointer:
A pointer is a variable that stores the address of another variable. It does not need to be initialized at the time of declaration and can be assigned later.

(2) Initialization

Reference:
A reference must be initialized when it is defined and must reference a valid object. Once bound to a variable, it cannot be changed to reference another object.

Example:

int a = 10;
int &b = a; // Must be initialized

Pointer:
A pointer does not need to be initialized when declared and can be assigned later. It can point to different objects over its lifetime.

Example:

int *p;       // Not initialized, points to an unknown location
int a = 10;
p = &a;       // Now points to `a`

(3) Changing What They Point To

Reference:
Once a reference is initialized, it cannot change the object it refers to.

Example:

int a = 10;
int &b = a;
// b = 20; // This changes the value of `a` to 20, but `b` still references `a`

Pointer:
A pointer can dynamically change the object it points to during runtime.

Example:

int a = 10;
int b = 20;
int *p = &a;  // `p` points to `a`
p = &b;       // Now `p` points to `b`

(4) Accessing the Object

Reference:
A reference provides direct access to the referenced object and has simpler syntax.

Example:

int a = 10;
int &b = a;
std::cout << b; // Direct access

Pointer:
A pointer requires the dereference operator (*) to access the object it points to.

Example:

int a = 10;
int *p = &a;
std::cout << *p; // Access through dereferencing

(5) Memory Size

Reference:
When used with sizeof, a reference returns the size of the object it refers to, as it does not occupy additional memory.

Example:

int a = 10;
int &b = a;
std::cout << sizeof(b); // Outputs sizeof(int)

Pointer:
The size of a pointer is fixed (usually 4 bytes on 32-bit platforms and 8 bytes on 64-bit platforms), regardless of the object it points to.

Example:

int *p;
std::cout << sizeof(p); // Outputs 4 or 8 depending on the platform

(6) Safety

Reference:
References cannot be NULL and do not encounter dangling reference issues, making them relatively safer.

Pointer:
Pointers can be null or dangling, which requires careful handling to avoid undefined behavior.

Example:

int *p = nullptr; // Null pointer
// int a = *p; // Dereferencing a null pointer leads to undefined behavior

II. inline

1. Definition

inline is a keyword in C++ used to suggest to the compiler to insert the function's code directly at the point of each call instead of using the standard function call mechanism. It is primarily used for small functions to reduce overhead.

2. Usage

Using inline in C++ is straightforward. Simply add the inline keyword before the function definition.

Example:

inline int add(int a, int b) {
    return a + b;
}

3. Advantages

  • Performance Improvement: Reduces function call overhead (e.g., stack push/pop) and improves performance, especially for small, frequently called functions.

  • Code Readability: Encourages modularity and improves code readability by allowing small functions to be written without performance concerns.

4. Considerations

  • Compiler's Decision: While you can suggest using inline, the compiler may choose not to inline the function based on complexity and other factors.

  • Code Bloat: If an inline function is called multiple times, its code is inserted at each call site, potentially increasing the size of the binary.

  • Debugging Complexity: Inlined functions may complicate debugging since call sites are replaced with the function body.

5. Suitable Scenarios

  • Small Functions: Functions with simple and small logic are ideal for inline.

  • Frequent Calls: Frequently called functions, such as those inside loops, may benefit from being inlined.


III. nullptr

In traditional C header files (e.g., stddef.h), NULL is defined as a macro, typically as follows:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif
  • In C++, NULL may be defined as the literal constant 0 or as a constant of type void* (as in C). Regardless of its definition, using NULL for null pointers can lead to certain issues. For instance, calling f(NULL) might unintentionally invoke the f(int) overload instead of the intended f(int*) overload because NULL is defined as 0, which matches the integer type. Using f((void*)NULL); can result in a compilation error.


nullptr in C++11

C++11 introduces nullptr, a special keyword representing a null pointer. It is a unique literal of its own type and can be implicitly converted to any pointer type. Using nullptr eliminates ambiguity in type conversions because it can only be implicitly converted to pointer types and cannot be converted to integer types.


Example:

#include <iostream>
using namespace std;

void f(int x)
{
    cout << "f(int x)" << endl;
}

void f(int* ptr)
{
    cout << "f(int* ptr)" << endl;
}

int main()
{
    f(0);          // Calls f(int x)
    
    // Intended to call f(int* ptr), but due to NULL being defined as 0, 
    // it instead calls f(int x), conflicting with the program's intent.
    f(NULL);
    
    f((int*)NULL); // Calls f(int* ptr)

    // Compilation error: “f”: none of the 2 overloads could convert all argument types
    // f((void*)NULL);

    f(nullptr);    // Correctly calls f(int* ptr)

    return 0;
}

Summary

References, inline functions, and nullptr are essential features in C++ that significantly impact code readability, performance, and safety. Understanding and leveraging these features appropriately can help you write efficient and maintainable code.

Hope this article has been helpful! Feel free to leave any questions or thoughts in the comments!