"C++ Advanced Programming" reading notes (11: Understanding flexible and peculiar C++)

1. References

2. It is recommended to read the book "21 Days to Learn C++" to get started. The link of the notes is as follows

1. Quote

  • In C++, a reference is an alias for another variable , and all modifications to a reference change the value of the referenced variable
    • References can be treated as implicit pointers without the hassle of taking variable addresses and dereferencing
    • A reference can also be used as another name for the original variable
    • You can create separate reference variables, use reference data members in classes, use references as parameters to functions and methods, and have functions or methods return references

1.1 Reference variables

  • Reference variables must be initialized when they are created
    int x = 3;
    int &xRef = x; // xRef 是 x 的另一个别名
    xRef = 10// 对 xRef 赋值会改变 x 的值
    
    int &emptyRef; // 错误,必须初始化
    
  • Cannot create a reference to an unnamed value (such as an integer literal) unless the reference is a const value
    int &unnamedRef1 = 5; // 无法编译
    const int &unnamedRef2 = 5; // 正常编译
    
  • cannot have non-const references to temporary objects, but can have const references
    std::string getString() {
          
          
        return "Hello world!";
    }
    
    std::string &string1 = getString(); // 无法编译
    const std::string &string2 = getString(); // 正常编译
    
1.1.1 Modify references
  • A reference always refers to the variable that was initialized. Once a reference is created, it cannot be modified.
    int x = 3, y = 4;
    int &xRef = x;
    xRef = y; // 将 y 的值 4 赋给 x,引用不会更新为指向 y
    
1.1.2 References to pointers and pointers to references
  • Can create references of any type, including pointer types
    int* intP;
    int* &ptrRef = intP; // ptrRef 是一个指向 intP 的引用,intP 是一个指向 int 值的指针
    ptrRef = new int;
    *ptrRef = 5;
    
  • The result of taking the address of a reference is the same as taking the address of a referenced variable
    • xPtr is a pointer to an int value and xRef is a reference to an int value
    int x = 3;
    int &xRef = x;
    int *xPtr = &xRef; // 取 x 引用(xRef)的地址,使 xRef 指向 x
    *xPtr = 100; // x 的值也变为 100
    

    Note: Cannot declare a reference to a reference or a pointer to a reference , e.g. int&& or int&* are not allowed

1.2 Reference data members

  • Data members of a class can be references
    • A reference cannot exist without pointing to another variable
    • So reference data members must be initialized in the constructor initializer, not in the constructor body
    class MyClass {
          
          
    public:
        MyClass(int &ref) : mRef(ref) {
          
          }
    private:
        int &mRef;
    };
    

1.3 Reference parameters

  • The default parameter passing mechanism is pass by value : the function receives copies of the parameters, and when these copies are modified, the original parameters remain unchanged

  • References allow specifying another semantics for passing arguments to functions: pass by reference . When using reference parameters, the function takes a reference as an argument. If the reference is modified, the original parameter variable is also modified . For example, a simple swap function is given below to swap the values ​​of two int variables

    • When the function swap() is called with x and y as parameters, the first parameter is initialized to a reference to x and the second parameter is initialized to a reference to y. When using swap() to modify first and second, x and y are actually modified as well
    void swap(int &first, int &second) {
          
          
        int temp = first;
        first = second;
        second = temp;
    }
    
    int x = 5, y = 6;
    swap(x, y);
    
  • Just as you cannot initialize a normal reference variable with a constant, you cannot pass a constant as an argument to a "pass by non-const reference" function

    swap(3, 4); // 编译错误
    
  • convert pointer to reference

    • When a function or method takes a reference as a parameter, and you have a pointer to the value being passed, you can dereference the pointer, "converting" the pointer to a reference. This behavior gives the value pointed to by the pointer, which the compiler then uses to initialize the reference parameter. For example, swap() can be called like this
    int x = 5, y = 6;
    int *xp = &x, *yp = &y;
    swap(*xp, *yp);
    

1.4 Using references as return values

  • You can also have a function or method return a reference, the main reason for doing this is for efficiency . Returning a reference to the object instead of the entire object avoids unnecessary copying. Of course, this trick can only be used if the objects involved survive the termination of the function
  • If the scope of the variable is limited to the function or method (such as a variable automatically allocated on the stack, which will be destroyed at the end of the function), a reference to this variable must not be returned

1.5 Rvalue references

  • An rvalue is a non-lvalue, such as a constant value, temporary object, or value. Normally, an rvalue is on the right side of an assignment operator
    void handleMessage(std::string &&message) {
          
          
        cout << "handleMessage with rvalue reference: " << message << endl;
    }
    
    handleMessage("Hello World");
    std::string a = "Hello ";
    std::string b = "World";
    handleMessage(a + b);
    

1.6 Use references or pointers

  • In C++, references can be considered redundant: almost everything that can be done with references can be done with pointers
    • However, references make programs easier to understand without invalid references and the need for explicit dereferences
    • There are also situations where pointers are required, one example is changing the location pointed to , since the variable to which a reference refers cannot be changed
    • When dynamically allocating memory, the result should be stored in a pointer, not a reference
    • Another situation that requires the use of pointers is optional parameters, that is, pointer parameters can be defined as optional parameters with a default value of nullptr , while reference parameters cannot be defined in this way, and another case is to store polymorphic types in containers
  • There is a way to tell whether to use pointers or references as parameter and return types: consider who owns the memory
    • If the code receiving the variable is responsible for freeing the associated object's memory, then it must use a pointer to the object, preferably a smart pointer, which is the recommended way of passing ownership
    • If the code receiving the variable does not need to free the memory, then it should use a reference

    Use references first. That is, use pointers only when references cannot be used

2. Questions about keywords

2.1 The const keyword

2.1.1 const variables and parameters
  • You can use const to "protect" a variable from being modified. An important use of this keyword is to replace #define to define constants

    const double PI = 3.1415926535;
    
  • You can also use const to specify that the parameters of a function or method remain unchanged

    void func(const int param) {
          
          
        // ...
    }
    
  • const pointer

    • to prevent modification of the pointed-to value ( constant pointer )
    int const *ip;
    ip = new int[10];
    ip[4] = 5; // 编译错误,无法修改 ip 所指的值
    
    • To prevent modification of the pointing pointer ( pointer constant )
    int* const ip = nullptr;
    ip = new int[10]; // 编译错误,无法修改 ip 的指向
    
    • To prevent modification of the pointed-to value and to prevent modification of the pointed-to
      • The first const is to the right of the int, so const is applied to the int pointed to by ip, thus specifying that the value pointed to by ip (the pointer) cannot be modified
      • The second const is to the right of the *, so the const is applied to the pointer to the int variable, which is the ip variable. Therefore, it is not possible to modify the ip (pointer) itself to point to
    int const * const ip = nullptr;
    
  • const reference

    • References are const by default , since you cannot change the object the reference refers to, you don't have to explicitly mark the reference as const
    • A reference to a reference cannot be created , so references usually have only one level of indirection
    int z;
    const int &zRef = z;
    zRef = 4; // 编译错误,由于将 const 应用到 int,因此无法对 zRef 赋值
    z = 5; // 但仍然可以修改 z 的值,直接改变 z 而不是通过引用
    
  • const references are often used as parameters. If you want to pass a value by reference to improve efficiency, but you don't want to modify the value, you can mark it as a const reference

    void doSomething(const BigClass &arg) {
          
           // 通常默认为 const 引用
        // ...
    }
    
2.1.2 const methods
  • A class method can be marked const to prohibit the method from modifying any non-mutable data members of the class
2.1.3 The constexpr keyword
  • C++ requires constant expressions in certain situations. For example, when defining an array, the size of the array must be a constant expression
    const int getArraySize() {
          
          
        return 32;
    }
    
    int main() {
          
          
        int myArray[getArraySize()]; // 无效
        return 0;
    }
    
    constexpr int getArraySize() {
          
           // 常量表达式
        return 32;
    }
    
    int main() {
          
          
        int myArray[getArraySize()]; // ok
        return 0;
    }
    

2.2 static keyword

2.2.1 Static data members and methods
  • Static data members and methods of a class can be declared. Static data members differ from non-static data members in that they are not part of the object . Instead, there is only one copy of this data member, which exists outside of any objects of the class
2.2.2 Static linking
  • Each source file for C++ is compiled separately, and the resulting object files are linked against each other. Every name in a C++ source file, including functions and global variables, has an internal or external linkage

    • External linkage means that the name is also valid in other source files, internal linkage (also known as static linking) means that the name is not valid in other source files
    • By default, both functions and global variables have external linkage. However, internal (or static) linkage can be specified using the keyword static in front of the declaration . For example, assume there are two source files, FirstFile.cpp and AnotherFile.cpp. Both source files compile successfully and the program links without problems: because the f() function has external linkage, the main() function can call this function from another file
    • Now suppose that static is applied to the f() function prototype in AnotherFile.cpp, there is no need to repeat the static keyword before the f() function definition. Each source file now compiles successfully, but fails at link time because the f() function has internal (static) linkage and cannot be called by FirstFile.cpp
    // FirstFile.cpp
    void f(); // 提供了原型但没给出定义
    
    int main() {
          
          
        f(); // f() 函数默认具有外部链接,可从 AnotherFile.cpp 调用该函数
        return 0;
    }
    
    // AnotherFile.cpp
    #include <iostream>
    void f(); // 提供了原型和定义
    // static void f(); // 此时 FirstFile.cpp 无法调用,因为 f() 为静态链接
    
    void f() {
          
          
        std::cout << "f\n";
    }
    
  • Another way to use static for internal linkage is to use anonymous namespaces , which can encapsulate variables or functions into a namespace without a name, instead of using static, as shown below

    • Items in a namespace can be accessed anywhere after the anonymous namespace is declared within the same source file, but not in other source files , with the same semantics as the static keyword
    • To get internal links, it is recommended to use anonymous namespaces instead of the static keyword
    #include <iostream>
    
    namespace {
          
          
        void f();
    
        void f() {
          
          
            std::cout << "f\n";
        }
    }
    
  • extern keyword

    • The extern keyword is the antonym of static, and the name after it is designated as an external link . This method can be used in some cases. For example, const and typedef are internally linked by default and can be made external using extern
2.2.3 Static variables in functions
  • The ultimate purpose of the static keyword in C++ is to create local variables that retain their value when leaving and entering scope . A static variable in a function is like a global variable that can only be accessed inside the function . The most common use of static variables is to "remember" whether a function performed a particular initialization
    void performTask() {
          
          
        static bool initialized = false;
        if (!initialized) {
          
          
            cout << "initialized" << endl;
            initialized = true;
        }
        // 执行期望的任务
    }
    
  • Single static variables should be avoided, instead objects can be used to maintain state

3. Types and type conversions

3.1 Type aliases

  • A type alias provides a new name for an existing type declaration, and variables created with the new type name are fully compatible with variables created with the original type declaration
    using IntPtr = int*;
    
  • The most common use of type aliases is to provide manageable names when the declaration of the actual type is too unwieldy , usually in templates
    using StringVector = std::vector<std::string>; // 类型别名可包括作用域限定符
    

3.2 Type aliases for function pointers

  • The location of functions in memory is usually not considered, but every function is actually located at some specific address. In C++, functions can be used like data. In other words, the address of the function can be used just like a variable
  • The type of the function pointer depends on the return type of the argument types of compatible functions. One way to handle function pointers is to use type aliases . Type aliases allow a type name to be assigned to a series of functions with specified characteristics. For example, the following line of code defines the MatchFunction type, which represents a pointer to any function that takes two int parameters and returns a Boolean value
    using MatchFunction = bool(*)(int, int);
    
    void findMatches(int values1[], int values2[], size_t numValues, MatchFunction matcher) {
          
          
        for (size_t i = 0; i < numValues; i++) {
          
          
            if (matcher(values1[i], values2[i])) {
          
          
                cout << "Match found at position " << i << 
                    " (" << values1[i] << ", " << values2[i] << ")" << endl;
            }
        }
    }
    
  • Since the intEqual() function matches the MatchFunction type, it can be passed as the last parameter of findMatches()
    bool intEqual(int num1, int num2) {
          
          
        return num1 == num2;
    }
    
    int arr1[] = {
          
          2, 5, 6, 9, 10, 1, 1};
    int arr2[] = {
          
          4, 4, 2, 9, 0, 3, 4};
    size_t arrSize = std::size(arr1); // C++17 前用法:sizeof(arr1) / sizeof(arr1[0]);
    cout << "Calling findMatches() using intEqual():" << endl;
    findMatches(arr1, arr2, arrSize, &intEqual);
    
    // 结果输出
    Calling findMatches() using intEqual():
    Match found at position 3 (9, 9)
    
  • Instead of using these old-style function pointers, you can also use std::function
  • Even though function pointers are uncommon in C++ (replaced by the virtual keyword), there are situations where it is necessary to obtain function pointers, perhaps obtaining function pointers in dynamic link libraries is the most common example

3.3 Type aliases for pointers to methods and data members

  • Pointers to methods and data members do not normally appear in programs. Remember however that you cannot dereference a pointer to a non-static method or data member without the object

3.4 typedef

  • Type aliases and typedefs are not exactly equivalent. Type aliases are more powerful when used with templates than typedefs
  • Always prefer type aliases over typedefs
    using IntPtr = int*typedef int* IntPtr; // 可读性较差
    
    using FunctionType = int(*)(char, double);
    typedef int(*FunctionType)(char, double); // 非常复杂
    

3.5 Type Conversion

3.5.1 const_cast()
  • const_cast() can be used to add constant characteristics to variables, or remove constant characteristics of variables
  • In theory, no const conversion is needed: if a variable is const, it should always be const. However, in practice, sometimes a function needs to take a const variable, but this variable must be passed to a function that takes a non-const variable as a parameter
    extern void ThirdPartyLibraryMethod(char* str);
    
    void f(const char* str) {
          
          
        ThirdPartyLibraryMethod(const_cast<char*>(str));
    }
    
3.5.2 static_cast()
  • Casts directly supported by the C++ language can be performed explicitly using static_cast()
    int i = 3;
    int j = 4;
    // 只需要把两个操作数之一设置为 double,就可确保执行浮点数除法
    double result = static_cast<double>(i) / j;
    
  • Another use of static_cast() is to perform downcasts in inheritance hierarchies
    • This type conversion can be used for pointers and references, not for objects themselves
    • static_cast() casts do not perform runtime type checking
    class Base {
          
          
    public:
        virtual ~Base() = default;
    };
    
    class Derived : public Base {
          
          
    public:
        virtual ~Derived() = default;
    };
    
    int main() {
          
          
        Base* b;
        Derived* d = new Derived();
        b = d;
        d = static_cast<Derived*>(b); // 需要向下转换
    
        Base base;
        Derived derived;
        Base& br = derived;
        Derived& dr = static_cast<Derived&>(br);
    
        return 0;
    }
    
  • static_cast() cannot convert a pointer of one type to a pointer of an unrelated other type
  • static_cast() cannot directly convert an object of one type to an object of another type if no converting constructor is available
  • static_cast() cannot convert a const type to a non-const type, and cannot convert a pointer to an int
3.5.3 reinterpret_cast()
  • reinterpret_cast() is more powerful and less secure than static_cast(). It can be used to perform type conversions that are not technically allowed by the C++ type rules, but are required by the programmer in some cases
    • For example, one reference type can be converted to another , even if the two references are unrelated
    • Likewise, one pointer type can be converted to another , even if the two pointers are not related in the inheritance hierarchy. This usage is often used to convert a pointer to void*, which can be done implicitly without requiring an explicit conversion. But converting a void* to a pointer of the correct type requires a reinterpret_cast(). A void* pointer points to a location in memory. void* pointer has no associated type information
    class X {
          
          };
    class Y {
          
          };
    
    int main() {
          
          
        X x;
        Y y;
        X* xp = &x;
        Y* yp = &y;
        xp = reinterpret_cast<X*>(yp);
    
        void* p = xp;
        xp = reinterpret_cast<X*>(P);
    
        X& xr = x;
        Y& yr = reinterpret_cast<Y&>(x);
    
        return 0;
    }
    

Be especially careful when using reinterpret_cast() as no type checking is performed when performing the cast

3.5.4 dynamic_cast()
  • dynamic_cast() provides runtime detection for type conversions within inheritance hierarchies . It can be used to convert pointers or references. dymamic_cast() detects the type information of the underlying object at runtime. If the type conversion does not make sense, dymamic_cast() will:
    • returns a void pointer (for pointers)
    • throws std::bad_cast exception (for quoting)
    class Base {
          
          
    public:
        virtual ~Base() = default;
    };
    
    class Derived : public Base {
          
          
    public:
        virtual ~Derived() = default;
    };
    
    Base* b;
    Derived* d = new Derived();
    b = d;
    d = dynamic_cast<Derived*>(b);
    

Runtime type information is stored in the object's vtable. Therefore, a class must have at least one virtual method in order to use dynamic_cast() . Attempting to use dynamic_cast() will result in a compilation error if the class does not have a vtable

4. Scope resolution

  • All names in a program, including variable, function, and class names, have some kind of scope. Scopes can be created using namespaces, function definitions, brace-delimited blocks, and class definitions
    #include <iostream>
    #include <memory>
    
    class Demo {
          
          
    public:
        static int get() {
          
          
            return 5;
        }
    };
    
    int get() {
          
          
        return 10;
    }
    
    namespace NS {
          
          
        int get() {
          
          
            return 20;
        }
    }
    
    int main() {
          
          
        auto pd = std::make_unique<Demo>();
        Demo d;
        std::cout << pd->get() << " ";
        std::cout << d.get() << " ";
        std::cout << NS::get() << " ";
        std::cout << Demo::get() << " ";
        std::cout << ::get() << " ";
        std::cout << get() << " ";
    }
    
    // 输出结果
    5 5 20 5 10 10
    

5. Features

5.1 [[noreturn]] feature

5.2 [[deprecated]] Features

5.3 [[fallthrough]] feature

5.4 [[nodiscard]] feature

5.5 The [[maybe_unused]] characteristic

5.6 Vendor-specific features

6. User-defined literals

  • C++ has many standard literals that can be used in code

    • 'a': character
    • "character array": \0-terminated character array (C-style string)
    • 3.14f: Floating-point numbers
    • 0xabc: hexadecimal value
  • C++11 allows you to define your own literals

    • User-defined literals should start with an underscore , and the first character after the underscore must be lowercase
    • Example: _i, _s, _km and _miles
  • C++ defines the following standard user-defined literals

    Note that these standard user-defined literals do not start with an underscore

    • "s" is used to create std::string
      auto myString = "Hello World"s;
      // 需要 using namespace std::string_literals;
      
    • "sv" is used to create std::string_view
      auto myStringView = "Hello World"sv;
      // 需要 using namespace std::string_view_literals;
      
    • "h" "min" "s" "ms" "us" "ns" is used to create std::chrono::duration time period
      auto myDuration = 42min;
      // 需要 using namespace std::chrono_literals;
      
    • "i" "il" "if" are used to create complex numbers complex<double>, complex<long double> and complex<float> respectively
      auto myComplexNumber = 1.3i;
      // 需要 using namespace std;
      

7. Header file

  • A header file is a mechanism for providing an abstract interface to a subsystem or piece of code
  • One thing to note when using header files is to avoid circular references or including the same header file multiple times
    • For example, suppose Ah includes Logger.h, which defines a Logger class; Bh also includes Logger.h. If you have a source file App.cpp that includes both Ah and Bh, you will end up with duplicate definitions of the Logger class because both Ah and Bh include the Logger.h header file
    • A file protection mechanism can be used to avoid duplicate definitions
    #ifndef LOGGER_H
    #define LOGGER_H
    
    class Logger {
          
          
        // ...
    };
    
    #endif
    
    • You can also use the #pragma once directive to replace the previous file protection mechanism
    #pragma once
    
    class Logger {
          
          
        // ...
    };
    
  • Forward declarations are another tool for avoiding header file problems . If you need to use a certain class, but cannot include its header file (for example, this class depends heavily on the currently written class), you can tell the compiler that such a class exists, but cannot use the #include mechanism to provide a formal definition
    • Can't really use this class in your code, because the compiler knows nothing about it, only that the named class exists after linking
    • But you can still use pointers or references to this class in your code
    • Functions may also be declared to return such forward-declared classes by value, or to pass such forward-declared classes as function parameters passed by value. Of course, the code that defines the function and any code that calls the function needs to add the correct header file, and the forward declaration class must be correctly defined in the header file
    • For example, suppose the Logger class uses another class, Preferences. The Preferences class uses the Logger class. Due to the circular dependency, it cannot be solved by the file protection mechanism. In this case, a forward declaration is required.
    #pragma once
    #include <string_view>
    
    class Preferences; // 前置声明
    
    class Logger {
          
          
    public:
        static void setPreferences(const Preferences& prefs);
        static void logError(std::string_view error);
    };
    

Guess you like

Origin blog.csdn.net/qq_42994487/article/details/131380363