banner
Lowerce

Lowerce

github
twitter

"C++ Primer" Reading Notes

C++ Primer#

  • Single File Compilation

g++ -o output_filename source_filename

  • Reading and Writing to Files

program_name < input_file

program_name > output_file

Data Types#

  • Selected Experience:

Choose unsigned type for non-negative numbers (unsigned)

Use int for integer operations; if exceeding the range, use long long

Use double for floating-point operations

  • Do not mix signed and unsigned types; signed numbers will automatically convert to unsigned, resulting in the remainder of the initial value modulo the total number of values representable by the unsigned type.
  • The type of character literal constants can be specified by a prefix; integer and floating-point types can be specified by a suffix.

Variables#

  • There is a fundamental difference between initialization and assignment.
  • Use list initialization {}; the compiler will issue a warning in case of potential data loss.
  • Declaration and definition are different; declaration makes the name known to the program, while definition allocates storage space or assigns an initial value to the variable.
  • Use the extern keyword to declare a variable.
  • A variable can and should only be defined once, but can be declared multiple times.
  • C++ is a statically typed language—types are checked at compile time.
  • Global variables can be explicitly accessed within block scope using the :: prefix (which hides local variables within the block).
  • It is best not to name local variables the same as global variables.

Composite Types#

  • Define reference types by adding & before the variable name; references must be initialized.
  • A reference is an alias.
  • A reference can only bind to an object.
  • Pointers store the address of an object; use the address-of operator & to get the address.
  • Access an object using a pointer; use the dereference operator * to access the object.
  • The meanings of & and * in declarations and expressions are entirely different.
  • Initialize null pointers (=nullptr, =0, =NULL).
  • Assignment always changes the object on the left side of the equals sign.
  • void* can store the address of any object.
  • Type modifiers (* and &) only modify the first variable identifier that follows them.

const Qualifier#

  • Constant references are references to const.
  • When initializing a constant reference, any expression can be used as the initial value, as long as the result of that expression can be converted to the type of the reference.
  • A const reference may refer to a non-const object.
  • A pointer or reference to a constant merely believes it points to a constant, thus it does not change the value of the pointed object.
  • The most effective way to understand the meaning of a declaration is to read from right to left.
  • Placing * before const indicates that the pointer is a constant—the unchanging part is the pointer's own value, not the value it points to.
  • A pointer being a constant does not mean that the value of the object it points to cannot be modified through the pointer.
  • Non-const can be converted to const, but not vice versa.
  • A constant expression is an expression whose value does not change and can be computed during the compilation process.
  • Whether an object is a constant expression is determined by its data type and initial value.
  • Top-level const indicates that the pointer itself is a constant; bottom-level const indicates that the object pointed to by the pointer is a constant (just an example; top-level and bottom-level const apply to various types).
  • The distinction between top-level const and bottom-level const is significant when performing copy operations on objects.

Handling Types#

  • Type alias: typedef former latter — the latter is a synonym for the former.
  • Pay attention to the use of * in typedef (it is not a simple replacement relationship); const modifies the given type.
  • Alias declaration: using former = latter — the former is a synonym for the latter.
  • auto automatically infers the data type based on the initial value (only retains bottom-level const); top-level const needs to be modified before auto.
  • * and & belong to a declaration and are not part of the basic data type.
  • decltype infers the type of an expression without using it as an initial value (retains the full type of the variable).
  • If the expression used in decltype is not a variable, decltype returns the type corresponding to the result of the expression.
  • If the content of the expression is a dereference operation, decltype will yield a reference type.
  • For expressions used in decltype, adding parentheses around the variable name will yield a different type than not adding them.
  • The result of decltype((variable)) is always a reference; decltype(variable) is only a reference if the variable itself is a reference.

Custom Data Structures#

  • struct ClassName ClassBody;
  • The preprocessor ensures that header files can be safely included multiple times—header guards.
  • #define sets a name as a preprocessor variable.
  • #ifdef is true if the variable is defined.
  • #ifndef is true if the variable is not defined.
  • #endif executes subsequent operations when the check is true until this command appears.
  • It is common to write preprocessor variable names in all uppercase to ensure their uniqueness.
  • Once a header file changes, related source files must be recompiled to obtain updated declarations.

using Declaration#

  • using namespace::name;
  • Header files should not contain using declarations.

string#

  • std::string is a variable-length character sequence.
  • When performing read operations, the string object automatically ignores leading whitespace (spaces, newlines, tabs, etc.) and starts reading from the first actual character until the next whitespace is encountered.
  • Common operations: getline(a,b) — reads a line from a, assigning it to b; s.empty() — checks if s is empty; s.size().
  • The return value of the size function is an unsigned integer (type string::size_type); be careful to avoid mixing int and unsigned.
  • String comparison: 1. When characters are the same, the shorter string is less than the longer string; 2. When characters differ, compare the first pair of differing characters.
  • When a string object and a character/string literal are mixed in a statement, ensure that at least one operand on either side of each + is a string.
  • String literals and strings are different types.
  • Use the C++ version of the C standard library header file ctype.h => cctype.
  • Cctype contains a series of character judgment and processing functions.
  • Range-based for statement for (declaration : expression) is similar to the for statement in Python.
  • Characters in a string can be accessed by index.
  • Always check the legality of indices (whether they are within the correct range).

vector#

  • std::vector represents a collection of objects (all objects of the same type), also known as a container.
  • Vector is a class template rather than a type.
  • vector<Type> container_name;
  • Vector has rich initialization methods: list (vector<T> v5{a,b,c...} or vector<T> v5={a,b,c...}), copy (vector<T> v2(v1) or vector<T> v2 = v1), construction (vector<T> v3(n,val) or vector<T> v3(n)), etc.
  • push_back(value) pushes the value as the tail element to the end of the vector.
  • Vectors can efficiently add elements quickly (there is no need to specify capacity).
  • If the loop body contains statements that add elements to the vector, range-based for loops cannot be used.
  • The empty and size functions are similar to those of strings.
  • The return value of the size function is also a special type of size_type for vectors, but it is necessary to specify the element type of the vector.
  • The comparison rules for vectors are similar to those for strings.
  • Vectors cannot use indices to add elements; they can only use indices to access existing elements.

Iterators#

  • Types with iterators also have members that return iterators.
  • begin() returns an iterator pointing to the first element; end() returns an iterator pointing to the position after the last element (past-the-end).
  • Generally, we do not know the exact type of the iterator (use auto to define the variable).
  • *iter returns a reference to the element pointed to by the iterator; iter->mem; ++iter/--iter indicates the next/previous element of the container.
  • Generic programming: All standard library container iterators define == and !=, so use != rather than < in for loops, as this programming style is valid for all containers provided by the standard library.
  • const_iterator can only read elements, not write them.
  • -> is a combination of dereferencing and member access; it->mem is equivalent to (*it).mem.
  • Any operation that may change the capacity of a vector will invalidate its iterators.
  • Any loop body that uses iterators should not add elements to the container to which the iterator belongs.
  • The result of subtracting two iterators is difference_type (a signed integer).

Arrays#

  • Similar to vectors, arrays are containers that store objects of the same type; the difference is that the size of an array is fixed and cannot be increased arbitrarily.
  • The number of elements in an array is also part of the array type, so it needs to be a constant expression.
  • Array initialization: list initialization, copying is not allowed.
  • Character arrays can be initialized using string literals, but note that string literals come with a terminating null character.
  • By default, type modifiers bind from right to left; but for arrays, it is more meaningful to read from inside (parentheses) out.
  • When using array indices, it is usually defined as size_t.
  • Using an object of array type is actually using a pointer to the first element of that array.
  • When using an array as an initial value for an auto variable, the inferred type is a pointer rather than an array; however, decltype will not perform this conversion.
  • Arrays can use indices to access non-existent elements that are after the last element.
  • begin(array_name)/end(array_name) can safely return pointers to the first element/past-the-end element.
  • The result of subtracting two pointers is ptrdiff_t.
  • If two pointers point to unrelated objects, they cannot be compared.
  • The index value used by the built-in subscript operator is not of unsigned type, which differs from vectors and strings.
  • C-style strings are stored in character arrays and terminated with a null character (\0).
  • Functions defined in the header file cstring can operate on C-style strings.
  • Using the standard library string is safer and more efficient than using C-style strings.
  • Prefer using standard library types over arrays.

Multidimensional Arrays#

  • Strictly speaking, there are no multidimensional arrays in C++; what is commonly referred to as a multidimensional array is actually an array of arrays.
  • Use a set of values enclosed in {} to initialize a multidimensional array; nested braces are completely equivalent (nesting is just for clearer reading).
  • You can initialize only part of the elements; other elements will be default initialized.
  • Use range-based for statements to process multidimensional arrays; all loop control variables except the innermost loop should be reference types.
  • When a program uses the name of a multidimensional array, it automatically converts it to a pointer to the first element of the array, which is a pointer to the first inner array.

Basics of Expressions#

  • Lvalues and rvalues: Lvalues can be on the left side of an assignment statement, while rvalues cannot (this is not so simple in C++).
  • When an object is used as an rvalue, the value (content) of the object is used; when an object is used as an lvalue, the identity (location in memory) of the object is used.
  • The assignment operator requires a non-const lvalue as its left operand, and the result is also an lvalue.
  • The address-of operator acts on an lvalue operand, returning a pointer to that operand, which is an rvalue.
  • The result of dereference operators and subscript operators is an lvalue.
  • In compound expressions, parentheses ignore precedence and associativity.
  • The order of evaluation is not explicitly defined for most operators, except for &&, ||, ?:, and ,.

Operators#

  • The quotient of integer division is always rounded towards zero (discarding the fractional part), regardless of whether it is positive or negative.
  • (-m)/n and m/(-n) are equivalent to -(m/n); m%(-n) is equivalent to m%n; (-m)%n is equivalent to -(m%n).
  • Avoid using the post-increment and post-decrement operators unless necessary.
  • ptr->mem is equivalent to (*ptr).mem. P.S. The precedence of the dereference operator is lower than that of the member access operator.
  • The conditional operator (cond?expr1:expr2) can be nested, but it is best not to exceed two or three layers.
  • The precedence of the conditional operator is very low, and parentheses are usually needed on both sides.
  • Use bitwise operators only for unsigned types.
  • The precedence of shift operators (IO operators) is neither high nor low: lower than arithmetic operators, higher than relational operators, assignment operators, and conditional operators.
  • sizeof returns the number of bytes occupied by an expression or a type name: sizeof (type) and sizeof expr.
  • sizeof does not actually compute the value of its operand.
  • Performing sizeof on char or expressions of type char results in 1.
  • The sizeof operation does not convert arrays to pointers; it is equivalent to performing sizeof on all elements of the array and summing the results.
  • Performing sizeof on string or vector objects only returns the size of the fixed part of that type and does not account for the space occupied by the elements within the object.
  • The true result of the comma operator is the value of the right-hand expression.

Type Conversion#

  • Arithmetic conversion: one arithmetic type to another; for example, the operands of operators will convert to the widest type, and integer values will convert to floating-point types.
  • Integer promotion: small integer types to larger integer types, such as bool, char, short promoted to int, long, etc.
  • Explicit type conversion cast-name<type>(expression).
  • Any type conversion that is clearly defined can use static_cast as long as it does not include bottom-level const.
  • static_cast is very useful when assigning a larger arithmetic type to a smaller type.
  • static_cast is also very useful for type conversions that the compiler cannot perform automatically.
  • const_cast can only change the bottom-level const of the operand; only const_cast can change the constant property of an expression.
  • Using reinterpret_cast is very dangerous.
  • Avoid explicit type conversions as much as possible.
  • Old-style explicit type conversions type (expr) and (type) expr are not clear and difficult to trace.

Operator Precedence#

Conditional Statements#

  • else matches the nearest unmatched if; using braces can enforce matching.
  • case labels must be integral constant expressions.
  • If a variable with an initial value is out of scope in one place and within scope in another, jumping from the former to the latter is illegal behavior.

Iteration Statements#

  • The execution flow of a traditional for loop: first execute the init-statement; next, check the condition; if true, execute the loop body; finally, execute the expression.
  • The init-statement in a for statement can define multiple objects, but only one declaration statement is allowed, so all variable base types must be the same.
  • In a range-based for statement, write operations on elements can only be performed when the range variable is a reference type.
  • Using auto in a range-based for statement ensures type compatibility.
  • The equivalent traditional for statement for a range-based for statement (you cannot use a range-based for statement to add elements to vector objects or other containers).
  • do while is very similar to while, except that it executes the loop body first and then checks the condition.

Jump Statements#

  • break terminates the nearest while, do while, for, or switch statement and continues execution from the first statement after these statements.
  • continue is used to terminate the current iteration of the nearest for, while, or do while loop and immediately begin the next iteration.
  • goto label; where label is an identifier for a statement.

try Statement Block and Exception Handling#

  • The exception detection part of the program uses the throw expression to raise an exception.
  • A try block is followed by one or more catch clauses, with the exception thrown in try selecting the corresponding catch clause.
  • C-style strings (const char*).
  • Exceptions interrupt the normal flow of the program; those programs that correctly execute "cleanup" during the occurrence of an exception are called exception-safe code.
  • The stdexcept header defines several common exception classes, along with a few other exception types: exception, bad_alloc, bad_cast.
  • The last three exceptions can only be initialized using default initialization; no initial value can be provided for these objects; other exception types can initialize these types of objects using string objects or C-style strings, but default initialization is not allowed.
  • The exception type defines only one member function what, which has no parameters and returns a const char* pointing to a C-style string that provides information about the exception.

Basics of Functions#

  • The parameter list in a function's parameter list is usually separated by commas, with each parameter containing a declaration of a declaration specifier; even if two parameters have the same type, both types must be written out.
  • Parameter names are optional; when a function indeed has individual parameters that will not be used, such parameters are usually unnamed to indicate they will not be used within the function body.
  • The return type of a function cannot be an array type or a function type, but it can be a pointer to an array or function.
  • Names have scope, and objects have lifetimes.
  • Parameters and variables defined within the function body are local variables, visible only within the function's scope, and will hide all other declarations with the same name in the outer scope.
  • Objects that exist only during the execution of a block become automatic objects.
  • Local static objects are initialized when the execution path of the program first passes through the object definition statement and are destroyed only when the program terminates—static.
  • Function names need to be declared before use; a function can only be defined once but can be declared multiple times; a function declaration does not require a function body, and ; can be used instead.
  • A function declaration is also called a function prototype.
  • Header files containing function declarations should be included in the source files defining the functions.
  • Separate compilation allows programs to be spread across several files, with each file compiled independently.

Parameter Passing#

  • Reference passing and value passing.
  • In C++, it is recommended to use reference type parameters instead of pointer types to access objects outside the function.
  • Use references to avoid copying.
  • When a function does not need to modify the value of a reference parameter, it is best to use a constant reference.
  • Use reference parameters to return additional information: a function can only return one value, but sometimes a function needs to return multiple values simultaneously; reference parameters provide an effective way to return multiple results at once.
  • Top-level const (which applies to the object itself) is ignored when initializing parameters with actual arguments.
  • In C++, it is allowed to define several functions with the same name, provided that the parameter lists of different functions have clear differences.
  • A bottom-level const object can be initialized with a non-const, but not vice versa; a normal reference must be initialized with an object of the same type.
  • C++ allows the use of literal values to initialize constant references.
  • Prefer using constant references; defining parameters that the function will not change as normal references is a common mistake.
  • The special nature of arrays: arrays cannot be copied; using arrays will convert them to pointers.
  • Although arrays cannot be passed by value, parameters can be written in a form similar to arrays, essentially passing a pointer to the first element of the array.
  • Three techniques for managing pointer parameters (array actual parameters): 1. Use a marker to specify the array length; 2. Use standard library conventions (pass pointers to the first element and the past-the-end element of the array); 3. Explicitly pass a parameter representing the size of the array.
  • Parameters can also be references to arrays, such as int (&arr)[10], where the size is part of the array type.
  • To pass multidimensional arrays, such as int matrix[][10], the compiler will ignore the first dimension; it is best not to include it in the parameter list. The declaration of matrix looks like a two-dimensional array, but the parameter is actually a pointer to an array containing 10 integers.
  • int main(int argc, char *argv[]) {...} The second parameter is an array whose elements are pointers to C-style strings; the first parameter indicates the number of strings in the array.
  • int main(int argc, char **argv) {...} is equivalent to the above code.
  • When using actual parameters from argv, optional parameters start from argv[1], with argv[0] storing the program name.
  • If the number of actual parameters for a function is unknown but all actual parameters have the same type, you can use a parameter of type initializer_list, which is used similarly to vector.
  • Elements in initializer_list are always constant values, and the values of the elements in the initializer_list object cannot be changed.
  • Ellipsis parameters are set to facilitate C++ programs to access certain special C code (using C standard library varargs), in the form of void foo(parm_list, ...); and void foo(n...).

Return Types and return Statements#

  • Functions that return void do not need to have a return statement, as it will be implicitly executed at the end. If you want to exit the function early, you can use return.
  • There should also be a return statement after a return statement in a loop; otherwise, the program is erroneous and difficult for the compiler to detect.
  • Do not return a reference or pointer to a local object.
  • Returning a reference yields an lvalue: calling a function that returns a reference gives an lvalue, while other return types yield rvalues.
  • C++11 specifies that functions can return a list of values enclosed in {} (return type is vector<Type>).
  • The header file cstdlib defines two preprocessor variables EXIT_FAILURE and EXIT_SUCCESS, which can be used as return values for the main function.
  • Recursion: a function calls itself (the main function cannot call itself).
  • Functions cannot return arrays but can return pointers or references to arrays.
  • To define a function that returns a pointer or reference to an array, you can use type aliases, such as typedef int arrT[10], or the equivalent using arrT = int[10], where arrT* func(int i) returns a pointer to an array containing 10 integers.
  • Besides type aliases, the function form that returns an array pointer is Type (*function(parameter_list))[dimension].
  • You can also use trailing return types, such as auto func(int i) -> int(*)[10].
  • Or use decltype(array_name)* to declare the function.

Function Overloading#

  • Overloaded functions: several functions with the same name in the same scope but different parameter lists (the main function cannot be overloaded).
  • The return types of overloaded functions must be consistent; it is not allowed for functions with the same name to return different types.
  • Top-level const parameters do not distinguish overloaded functions, while bottom-level const (pointers, references) can distinguish overloaded functions.
  • It is best to only overload very similar operations.
  • const_cast is most useful in the context of overloaded functions.
  • Function matching is also called overload resolution; there are three possible results when calling an overloaded function: best match, no match, ambiguous call.
  • In C++, name lookup occurs before type checking.

Special Purpose Language Features#

  • Once a parameter is given a default value, all subsequent parameters must have default values.
  • Default actual parameters fill in the missing trailing actual parameters in a function call.
  • When designing functions with default actual parameters, one task is to reasonably set the order of parameters, placing those that do not often use default values earlier and those that frequently use default values later.
  • In a given scope, a parameter can only be assigned a default actual parameter once; subsequent declarations of the function can only add default actual parameters to those parameters that did not previously have default values, and all parameters to the right of that parameter must have default values.
  • Default actual parameters should be specified in the function declaration and placed in the appropriate header file.
  • Any expression whose type can be converted to the required type of the parameter can be used as a default actual parameter; the names used as default actual parameters are resolved in the scope where the function declaration is located, while the evaluation process of these names occurs during the function call.
  • Inline functions can avoid the overhead of function calls.
  • Adding inline before the return type declares it as an inline function.
  • Inline specifications are merely requests to the compiler, which may choose to ignore this request.
  • The inline mechanism is generally used to optimize small, straightforward functions that are called frequently.
  • constexpr functions are functions that can be used in constant expressions, where the return type and all parameter types must be literal types, and the function body contains only one return statement.
  • During the compilation process, constexpr functions are implicitly specified as inline functions.
  • It is allowed for the return value of a constexpr function not to be a constant.
  • Inline functions and constexpr functions are usually placed in header files.
  • The assert preprocessor macro is used as follows: assert (expr); it evaluates expr, and if false, outputs information and terminates execution; if true, does nothing.
  • Preprocessor names are managed by the preprocessor manager rather than the compiler, and preprocessor names should be used directly without needing a using declaration.
  • The behavior of assert depends on the state of the NDEBUG preprocessor variable; when #define NDEBUG is present, assert does nothing.
  • The compiler defines some local static variables for program debugging: __func__, __FILE__, __LINE__, __TIME__, __DATE__.

Function Matching#

  • Candidate functions: functions with the same name, declarations visible.
  • Viable functions: equal number of parameters, same parameter types.
  • Finding the best match.
  • If no function stands out, the compiler will refuse the request due to ambiguity in the call.
  • Avoid forced type conversions when calling overloaded functions. If forced type conversion is necessary in practical applications, it indicates that the designed parameter set is unreasonable.
  • The levels of actual parameter type conversion: 1. Exact match; 2. Match achieved through const conversion; 3. Match achieved through type promotion; 4. Match achieved through arithmetic type conversion or pointer conversion; 5. Match achieved through class type conversion.
  • Built-in type promotion and conversion may yield unexpected results during function matching.
  • All arithmetic type conversions have the same level.

Function Pointers#

  • Function pointers point to functions rather than objects.
  • Function bool lengthCompare(const string &, const string &); declares a pointer to that function as bool (*pf)(const string &, const string &);.
  • pf = lengthCompare; is equivalent to pf = &lengthCompare.
  • bool b = pf("hello","goodbye"); is equivalent to bool b = (*pf)("hello","goodbye"); and bool b = lengthCompare("hello","goodbye");.
  • Similar to arrays, although function type parameters cannot be defined, parameters can be pointers to functions; the parameter appears to be of function type but is treated as a pointer; functions can be used directly as actual parameters, and they will automatically convert to pointers.
  • Using type aliases and decltype can simplify the use of function pointers, such as typedef decltype(lengthCompare) Func; defines the function type, and typedef decltype(lengthCompare) *FuncP; defines a pointer to the function type.
  • using F = int(int*, int); defines the function type F, and using PF = int(*)(int*, int); defines a pointer to the function type.

Defining Abstract Data Types#

  • Class = Data Abstraction + Encapsulation; Data Abstraction = Interface + Implementation.
  • Functions defined within a class are implicitly inline functions.
  • Member function declarations must be inside the class; their definitions can be inside or outside the class; non-member functions that are part of the interface are defined and declared outside the class.
  • Member functions access the object that calls them through an additional implicit parameter named this. When we call a member function, the address of the object requesting that function initializes this.
  • Because the purpose of this is always to point to "this" object, this is a constant pointer, and the address stored in this cannot be changed.
  • When the const keyword is placed after the parameter list of a member function, the const immediately following the parameter list modifies the type of the implicit this pointer, indicating that this is a pointer to const => member functions that use const in this way are called constant member functions.
  • Constant member functions cannot change the contents of the object that calls them.
  • Constant objects, as well as references or pointers to constant objects, can only call constant member functions.
  • The compiler first compiles the declarations of members before moving on to the member function bodies. Therefore, member function bodies can freely use other members of the class without regard to the order in which those members appear.
  • The definition of a member function must match its declaration, and the names of members defined outside the class must include the name of the class to which they belong.
  • return *this returns the object that called the function; the return type of the function should be a reference of the corresponding type.
  • If a non-member function is part of the class interface, those functions should be declared in the same header file as the class.
  • Constructors cannot be declared as const.
  • The constructor created by the compiler is also called the synthesized default constructor.
  • Only when a class has not declared any constructors will the compiler automatically generate a default constructor.
  • If a class contains members of built-in types or conforming types, the class can only use the synthesized default constructor if all these members are fully initialized within the class.
  • = default requests the compiler to generate a default constructor.
  • The constructor's initializer list appears after function_name(parameter_list): and before the {} function body.
  • The constructor's initializer list is a list of member names, each followed by () containing the member's initial value, separated by commas for different members.
  • When a data member is ignored by the constructor's initializer list, it will be implicitly initialized in the same way as the synthesized default constructor.
  • Generally, the compiler-generated copy, assignment, and destruction operations will perform copy, assignment, and destruction operations on each member of the object.
  • Many classes that require dynamic memory should use vector or string objects to manage the necessary storage space, as using vector or string can avoid the complexities of allocating and releasing memory.
  • If a class contains vector or string members, its copy, assignment, and destruction synthesized versions will work correctly.

Access Control and Encapsulation#

  • Use access specifiers (public, private) to enhance the encapsulation of classes.
  • The only difference between the class and struct keywords is that their default access permissions are different; either can be used to define a class.
  • struct: members before the first access specifier are public; class: members before the first access specifier are private.
  • A class can allow other classes or functions to access its non-public members -> friend.
  • A friend declaration only requires adding a function declaration statement starting with the friend keyword inside the class.
  • Friend declarations apply to non-member functions that are part of the class interface.
  • Friend declarations only specify access permissions, not function declarations in the usual sense. If we want users of the class to be able to call a friend function, we must declare the function separately outside the friend declaration.
  • Generally, friend functions should be independently declared outside the class, in addition to the friend declaration within the class.
  • It is best to concentrate friend declarations at the beginning or end of the class definition.

Other Features of Classes#

  • Members used to define types must be defined before use, so type members usually appear at the beginning of the class.
  • It is best to only specify inline in places defined outside the class, as this can make the class easier to understand.
  • Member functions can be overloaded as long as there are differences in the number and/or types of parameters.
  • When we want to modify a certain data member of a class, even within a const member function, we can achieve this by adding mutable to the variable declaration.
  • When providing an initial value within a class, it must be indicated with = or {}.
  • A const member function that returns *this as a reference will have a return type of constant reference.
  • By distinguishing whether a member function is const, we can perform overloading.
  • Recommendation: use private utility functions for public code -> avoid using the same code in multiple places.
  • We can declare a class without defining it temporarily (similar to functions), such as class Screen; -> forward declaration.
  • A class type that is "declared after" and "defined before" is an incomplete type.
  • Forward declarations apply to when a class member contains a reference or pointer to its own type.
  • If a class specifies a friend class friend class ClassName, then the member functions of the friend class can access all members of this class, including non-public members.
  • Each class is responsible for controlling its own friend classes or friend functions; friend relationships are not transitive.
  • When declaring a member function as a friend, it is necessary to specify which class the member function belongs to, such as ClassName::member_function_name.
  • To make a member function a friend, we must carefully organize the structure of the program to satisfy the mutual dependency between declarations and definitions.
  • If a class wants to declare a group of overloaded functions as its friends, it needs to declare each function in that group individually.

Scope of Classes#

  • The fact that a class is a scope explains why we must provide both the class name and function name when defining member functions outside the class.
  • Once a class name is encountered, the remainder of the definition is within the class's scope.
  • The return type must specify which class's member it is (the names used in the return type are outside the class's scope).
  • The compiler processes all declarations within the class before processing the definitions of member functions.
  • Inside a class, if a member uses a name from the outer scope that represents a type, the class cannot redefine that name afterward.
  • When class members are hidden, you can force access to members by adding the class name or explicitly using the this pointer, such as this->member_variable_name or ClassName::member_variable_name.
  • It is advisable not to use member names as parameters or other local variables.
  • When an object from the outer scope is hidden, you can use the scope operator to access it.

Further Exploration of Constructors#

  • Initialization and defining followed by assignment can be significantly different in some cases.
  • If a member is const or a reference or belongs to a certain class type and that class does not define a default constructor, it must be initialized.
  • It is good practice to use constructor initializers.
  • The order of initialization of members is consistent with the order in which they appear in the class definition; it is best to ensure that the order of constructor initializers matches the order of member declarations, and if possible, avoid using certain members to initialize other members.
  • If a constructor provides default actual parameters for all parameters, it effectively also defines a default constructor.
  • Delegating constructors use other constructors of their class to perform their own initialization process, with their member initializer list having only one entry, namely the class name, such as Sales_data(): Sales_data("", 0, 0){function_body}.
  • When an object is default initialized or value initialized, the default constructor is automatically executed; the class must contain a default constructor for these cases to be used.
  • If other constructors are defined, it is best to also provide a default constructor.
  • The compiler will only automatically perform one step of class type conversion.
  • explicit can be used to suppress implicit conversions of constructors; it can only be used when declaring constructors within the class, and explicit only applies to constructors with one actual parameter; constructors requiring multiple actual parameters cannot be used for implicit conversions and do not need to be specified.
  • When a constructor is declared with the explicit keyword, it can only be used in direct initialization form, and the compiler will not use that constructor during automatic conversions.
  • Although the compiler will not use explicit constructors for implicit conversions, we can explicitly force conversions using such constructors, such as static_cast.
  • Aggregate classes: 1. All members are public; 2. No constructors are defined; 3. No initial values within the class; 4. No base class and no virtual functions.
  • Aggregate classes can be initialized using a member initializer list enclosed in braces, such as Data val1 = { 0, "Anna" }, where the order of initial values must match the order of declarations; if the number of elements in the initializer list is less than the number of members in the class, the later members are value initialized.
  • An aggregate class where all data members are literal types is a literal constant class.
  • If:
    • All data members are literal types;
    • The class contains at least one constexpr constructor;
    • If a data member has an initial value within the class, the initial value of built-in type members is a constant expression, or if the member belongs to a certain class type, the initial value uses the member's own constexpr constructor;
    • The class must use the default definition of the destructor.
      Then it is also a literal constant class.
  • The body of a constexpr constructor is generally empty, and a constexpr constructor can be declared using the prefix keyword.

Static Members of Classes#

  • Using the static keyword makes a member directly related to the class itself rather than associated with each object of the class.
  • Static members of a class exist outside of any object; objects do not contain any data related to static data members.
  • Static member functions are not bound to any object and do not contain a this pointer; they cannot be declared as const.
  • Static members can be accessed directly using the scope operator, or they can be accessed using an object, reference, or pointer to the class; member functions can directly use static members without needing the scope operator.
  • Static member functions can be defined both inside and outside the class; when defined outside the class, the static keyword cannot be repeated.
  • Each static member must be defined and initialized outside the class.
  • From the class name onward, the remainder of a definition statement is within the class's scope.
  • The type of static data members can be of the class type to which they belong, while non-static data members can only be declared as pointers or references to the class they belong to.
  • Another important distinction between static members and ordinary members is that we can use static members as default actual parameters, but non-static members cannot, as their values are part of the object.

IO Classes#

  • The headers iostream, fstream, and sstream define types for reading and writing streams, named files, and memory string objects, respectively.
  • The standard library allows us to ignore the differences between these different types of streams through inheritance mechanisms.
  • Cannot copy or assign IO objects.
  • The condition state of IO classes; the simplest way to determine the state of a stream object is to use it as a condition, such as checking the state of the stream returned by the >> expression in a while loop.
  • The rdstate member of a stream object returns an iostate value corresponding to the current state of the stream.
  • The clear member of a stream object can reset all error flags (no parameters) or set a new state for the stream (with parameters).
  • Buffer flushing: data is actually written to the output device or file; there are many reasons that can cause a buffer to flush.
  • endl completes a newline and flushes the buffer; flush flushes the buffer without outputting any additional characters; ends inserts a null character into the buffer and flushes the buffer.
  • If the program crashes, the output buffer will not be flushed.
  • cout<<unitbuf; tells the stream to perform a flush after each write operation; cout<<nounitbuf; restores the normal buffer flushing mechanism.
  • When an input stream is associated with an output stream, reading data from the input stream will first flush the associated output stream; cout and cin are associated with each other.
  • x.tie(&o) associates stream x with output stream o.
  • You can associate istream with ostream, or ostream with ostream.
  • Each stream can be associated with only one stream at a time, but multiple streams can be associated with the same ostream.

File Input and Output#

  • The header file fstream defines three types to support file IO: ifstream - read; ofstream - write; fstream - read/write.
  • Unique operations for fstream.
  • ifstream in(ifile); constructs an ifstream and opens the given file.
  • Calling open can associate an empty file stream with a file, for example, ofstream out; and out.open(ofile);. Use if (out) to check if open was successful.
  • To associate a file stream with another file, you must first close the already associated file, such as in.close() and in.open(ifile).
  • When an fstream object is destroyed, close is automatically called.
  • File modes: in - read mode; out - write mode; app - position at the end of the file before each write operation; ate - position at the end of the file after opening; trunc - truncate the file; binary - perform IO in binary mode.
  • Opening a file in out mode (the default mode for ofstream) will discard existing data.
  • To prevent an ofstream from clearing the contents of a given file, specify the app mode simultaneously, for example, ofstream app("file", ofstream::out | ofstream::app);.
  • Each time a file is opened, the file mode must be set; otherwise, the default value will be used.

String Streams#

  • istringstream - read; ostringstream - write; stringstream - read/write.
  • Unique operations for stringstream.
  • Usage of istringstream and ostringstream.
  • String streams are very useful when splitting strings read from files.

Overview of Sequential Containers#

  • vector - variable-sized array; deque - double-ended queue; list - doubly linked list; forward_list - singly linked list; array - fixed-size array; string - similar to vector, but specifically for storing characters.
  • string and vector store elements in contiguous memory space: calculating addresses by element index is very fast, but adding or deleting elements in the middle is very time-consuming.
  • list and forward_list make adding and deleting operations at any position in the container very fast, but do not support random access to elements, with significant additional memory overhead.
  • deque is more complex, supporting fast random access, but adding or deleting elements in the middle is costly, while adding or deleting elements at both ends is fast.
  • array has a fixed size and does not support adding or deleting elements or changing the size of the container.
  • Modern C++ programs should use standard library containers rather than raw data structures like built-in arrays.
  • Unless there is a good reason to choose other containers, use vector.
  • If the program has many small elements and additional space overhead is important, do not use list or forward_list.
  • If the program requires random access to elements, use vector or deque.
  • If the program requires adding or deleting elements in the middle of the container, use list or forward_list.
  • If the program needs to add or delete elements at both ends but does not require adding or deleting elements in the middle, use deque.
  • If the program only needs to insert elements in the middle of the container when reading input, and subsequently needs random access to elements: first determine whether it is really necessary to insert elements in the middle of the container; while processing input data, it is easy to append data to vector, then call the standard library's sort function to rearrange the elements in the container, thus avoiding inserting elements in the middle; if it is necessary to insert elements in the middle, consider using list during the input phase, and once the input is complete, copy the contents of the list to vector.
  • If unsure which container to use, you can use only the common operations of vector and list in the program: use iterators, avoid subscript operations, and avoid random access. This way, it will be convenient to choose to use either vector or list when necessary.

Overview of Container Libraries#

  • Each container is defined in a header file, with the filename matching the type name. Containers are defined as template classes, and most containers require additional information about the element type.

  • Sequential containers can almost store elements of any type.

  • Container operations: type aliases, constructors, assignment and swap, size, adding and deleting elements, obtaining iterators, additional members of reverse containers.

  • The range of iterators is represented by a pair of iterators, [begin,end) is a left-closed, right-open interval, and it is necessary to ensure that end is not before begin and that both point to elements of the same container or the past-the-end element.

  • With the help of type aliases, you can use a container without knowing the element type, which is very useful in generic programming.

  • The begin and end operations generate iterators pointing to the first element and the past-the-end element of the container, forming a range of iterators that includes all elements in the container.

  • begin and end have multiple versions:

    list<string> a = {"Milton", "Shakespeare", "Austen"};
    auto it1 = a.begin(); // list<string>::iterator
    auto it2 = a.rbegin(); // list<string>::reverse_iterator
    auto it3 = a.cbegin(); // list<string>::const_iterator
    auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
    
  • When write access is not needed, cbegin and cend should be used.

  • Definition and initialization of containers: default constructor, copy initialization c1(c2) or c1=c2, list initialization c{a,b,c...} or c={a,b,c...}. Only sequential containers' constructors can accept size parameters seq(n,t); associative containers do not support this.

  • Copy initialization: 1. Copy the entire container; 2. Copy the elements specified by a pair of iterators.

  • When initializing one container as a copy of another, both containers must have the same container type and element type.

  • Sequential containers provide constructors that can accept a container size and an initial value for elements, such as vector<int> ivec(10,-1);.

  • When defining an array, in addition to specifying the element type, the size must also be specified, such as array<int, 42>.

  • Although we cannot perform copy or assignment operations on built-in array types, array does not have this restriction.

  • Assignment operations can be used for all containers.

  • Assignment operations replace all elements of the left container with copies of the elements of the right container.

  • swap (to swap elements) is usually much faster than direct copying, such as swap(c1,c2) or c1.swap(c2).

  • assign (to replace elements) is only applicable to sequential containers and does not support associative containers and array, such as seq.assign(b,e), seq.assign(il), seq.assign(n,t).

  • Assignment-related operations will invalidate the internal iterators, references, and pointers of the container, but swap will not.

  • Except for array, swap does not copy, delete, or insert any elements, ensuring it completes in constant time.

  • Except for string, iterators, references, and pointers pointing to containers still point to the elements they pointed to before the swap operation after the swap.

  • The time required to swap two array objects is proportional to the number of elements in the array.

  • It is a good habit to consistently use the non-member version of swap.

  • Container size operations: size, empty, max_size.

  • Comparing two containers actually performs pairwise comparisons of elements, similar to the relational operations of string: if the sizes are the same and elements are equal, they are equal; if the sizes differ and elements are equal, the smaller container is less than the larger container; if the sizes differ and elements are not equal, it depends on the comparison result of the first unequal element.

Operations on Sequential Containers#

  • Adding elements: push_back(t) or emplace_back(args), push_front(t) or emplace_front(args), insert(p,t) or emplace(p,args), and various insert operations.
  • Inserting elements into a vector, string, or deque will invalidate all iterators, references, and pointers pointing to the container.
  • Every sequential container except array and forward_list supports push_back.
  • When we use an object to initialize a container or insert an object into a container, what is actually placed into the container is a copy of the object's value, not the object itself.
  • list, forward_list, and deque also support push_front; deque provides the ability to randomly access elements like vector, but it offers push_front, which vector does not support, ensuring that adding and deleting elements at both ends of the container only takes constant time, while inserting elements outside the ends will be very time-consuming.
  • It is legal to insert elements into any position in vector, deque, or string, but it may be time-consuming.
  • The return value of insert is an iterator pointing to the newly inserted element.
  • Understanding emplace: c.emplace_back("978-0590353403", 25, 15.99) is equivalent to c.push_back(Sales_data("978-0590353403", 25, 15.99)).
  • emplace constructs elements directly in the container. The parameters passed to emplace must match the constructor of the element type.
  • front and back return references to the first and last elements, respectively; note the distinction between these and begin and end (the latter are iterators).
  • Member functions returning references; if the container is a const object, the return value is a const reference; if the container is not const, the return value is a normal reference, which we can use to change the value of the element.
  • If using auto variables to save and change the values of elements, the variables must be defined as reference types.
  • The at and subscript operations are only applicable to string, vector, deque, and array; each sequential container has a front member function, and all sequential containers except forward_list have a back member function.
  • Deleting any element from deque except the first and last will invalidate iterators, references, and pointers; iterators, references, and pointers pointing to positions after the deletion point in vector or string will also be invalidated.
  • pop_front and pop_back delete the first and last elements, respectively; vector and string do not support pop_front, while forward_list does not support pop_back.
  • erase can delete a single element specified by an iterator or delete all elements in a range specified by a pair of iterators, returning an iterator pointing to the position after the deleted element.
  • Inserting and deleting operations for forward_list: before_begin() and cbefore_begin() return iterators pointing to a non-existent element before the first element of the list (before-begin iterator); insert_after() inserts an element after the iterator p; emplace_after creates an element at the position specified by p using args; erase_after deletes the element after the position pointed to by p.
  • Resizing sequential containers: c.resize(n), c.resize(n,t); resizing vector, string, and deque may invalidate iterators, pointers, and references; when shrinking the container, iterators, references, and pointers pointing to deleted elements will be invalidated.
  • Since adding elements to iterators and deleting elements from iterators may invalidate the iterators, it is necessary to ensure that iterators are correctly repositioned after each operation that changes the container, which is particularly important for vector, string, and deque.
  • The program must ensure that iterators, references, or pointers are updated in each loop step, and using insert and erase is a good choice, as they return iterators after the operation (pointing to newly added elements and after deleted elements).
  • Do not save iterators returned by end, but repeatedly call it.

How vector Objects Grow#

  • vector stores elements contiguously; to reduce the overhead of memory allocation and release when adding elements, vector and string will preallocate larger memory spaces to avoid reallocating memory space.
  • Member functions managing capacity: c.capacity() - how many elements c can hold without reallocating memory; c.shrink_to_fit() - reduces capacity() to be the same size as size(); c.reserve(n) - allocates memory space to accommodate n elements.
  • Calling reserve will never reduce the memory space occupied by the container.

Additional String Operations#

  • Other methods for constructing strings: string s(cp,n), string s(s2,pos2), string s(s2,pos2,len2).
  • s.substr(pos,n) substring operation returns a copy of n characters starting from pos in s.
  • string also defines additional versions of insert and erase (versions that accept indices): s.insert(s.size(), 5, 'i');, s.erase(s.size()-5, 5);.
  • string also provides insert and assign that accept C-style character arrays: s.assign(cp,7);, s.insert(s.size(),cp+7);, where cp is a const char*.
  • The string class also defines append and replace to change the contents of the string; s.append(string) inserts at the end, s.replace(pos,n,string) replaces the content starting at pos for n characters.
  • The find function performs the simplest search, returning the index of the first matching position, or string::npos if not found.
  • find_first_of, find_first_not_of, find_last_of, find_last_not_of find positions matching any character in the given string.
  • A common programming pattern is to use this optional starting position to loop through the string to search for the occurrence of a substring.
  • Use rfind and find_last for reverse searching.
  • The compare function is similar to strcmp.
  • Value conversion: to_string converts values to string, stod, stof, stold, stoi, stol, stoul, stoll, stoull convert string to values.
  • The first non-whitespace character in the string to be converted to a value must be a valid numeric character: d = stod(s2.substr(s2.find_first_of("+-.0123456789"))).

Container Adapters#

  • Essentially, an adapter is a mechanism that allows one thing to behave like another. A container adapter accepts an existing container type and makes its behavior appear like a different type.
  • There are some operations and types that all adapters support.
  • Three sequential container adapters: stack, queue, priority_queue, which by default are based on deque for the first two and on vector for the latter.
  • Assuming deq is a deque<int>, you can initialize a stack with stack<int> stk(deq);.
  • An empty stack implemented on vector: stack<string, vector<string>> str_stk;.
  • The stack is implemented based on deque by default, but can also be implemented on list or vector.
  • Other operations for the stack: s.pop() deletes the top element but does not return its value; s.push(item)/s.emplace(args) creates a new element pushed onto the top of the stack, which comes from copying or moving item, or constructed from args; s.top() returns the top element but does not pop it from the stack.
  • The queue is also implemented based on deque by default, and the priority_queue is implemented based on vector by default.
  • The queue can also be implemented on list or vector, and the priority_queue can also be implemented on deque.
  • Other operations for the queue: q.pop() returns the first element of the queue or the highest priority element of the priority_queue, but does not delete this element; q.front()/q.back() returns the first or last element, but does not delete this element, only applicable to queue; q.top() returns the highest priority element but does not delete it, only applicable to priority_queue; q.push(item)/q.emplace(args) creates an element at the end of the queue or in the appropriate position in the priority_queue, with the value being item or constructed from args.

Overview of Generic Algorithms#

  • Most algorithms are defined in the header file algorithm. The standard library also defines a set of numerical generic algorithms in the header file numeric.
  • Iterators allow algorithms to be independent of containers.
  • However, algorithms depend on the operations of the container type.
  • Algorithms never perform operations on containers; they only operate on iterators and execute operations on iterators.

Introduction to Generic Algorithms#

  • For algorithms that only read and do not change elements, it is usually best to use cbegin() and cend(). However, if you plan to use iterators returned by the algorithm to change the values of elements, you need to use the results of begin() and end() as parameters.
  • Algorithms that only accept a single iterator to represent a second sequence assume that the second sequence is at least as long as the first sequence.
  • Examples of read-only algorithms: find, count, accumulate, equal.
  • Algorithms do not perform container operations, so they cannot change the size of the container.
  • Algorithms that write data to the destination iterator assume that the destination has enough space to accommodate the elements to be written; algorithms do not check for write operations.
  • One way to ensure that an algorithm has enough element space to accommodate output data is to use insert iterators. An insert iterator is an iterator that adds elements to a container.
  • back_inserter defined in iterator accepts a reference to a container and returns an insert iterator bound to that container.
  • Examples of write algorithms: fill, fill_n, copy, replace, replace_copy.
  • Standard library algorithms operate on iterators rather than containers. Therefore, algorithms cannot (directly) add or remove elements.
  • Examples of rearrangement algorithms: unique.

Custom Operations#

  • You can use user-defined operations to replace default operators to implement the default behavior of algorithms.
  • A predicate is a callable expression whose return result is a value that can be used as a condition.
  • stable_sort maintains the original order of equal elements.
  • A lambda expression represents a callable unit of code. We can think of it as an unnamed inline function. Like any function, a lambda has a return type, a parameter list, and a function body. However, unlike functions, lambdas can be defined inside functions.
  • A lambda expression has the following form: [capture list] (parameter list) -> return type { function body }.
  • We can omit the parameter list and return type, but the capture list and function body must always be included.
  • If the function body of a lambda contains anything other than a single return statement and does not specify a return type, it returns void.
  • A lambda can use a local variable from the function in which it is defined only if that variable is captured in its capture list.
  • You can use lambdas to customize search conditions or operations for find_if, for_each.
  • The capture list is only used for local non-static variables; lambdas can directly use local static variables and names declared outside the function.
  • The capture method for variables can also be by value [v] or by reference [&v].
  • Reference capture is necessary when dealing with streams, and it must be ensured that the variable exists when the lambda executes.
  • Generally, we should try to minimize the amount of data captured to avoid potential capture-related issues. Moreover, if possible, we should avoid capturing pointers or references.
  • To indicate to the compiler to deduce the capture list, write a & or = in the capture list. & tells the compiler to use reference capture, while = indicates value capture.
  • If we want to capture some variables by value and others by reference, we can mix implicit and explicit captures, separated by a comma.
  • When mixing implicit and explicit captures, the first element in the capture list must be either & or =. This symbol specifies the default capture method as reference or value.
  • When mixing implicit and explicit captures, the explicitly captured variables must use a different method than the implicit captures.
  • If we want to change the value of a captured variable, we must add the keyword mutable at the beginning of the parameter list. Mutable lambdas can omit the parameter list.
  • For simple operations that are only used in one or two places, lambda expressions are most useful. If we need to use the same operation in many places, it is usually better to define a function rather than repeatedly writing the same lambda expression. Similarly, if an operation requires many statements to complete, using a function is generally better.
  • If the capture list of a lambda is empty, it can usually be replaced with a function. However, replacing a lambda that captures local variables with a function is not so easy.
  • The bind standard library function, defined in functional, can be viewed as a general function adapter that takes a callable object and generates a new callable object to adapt the parameter list of the original object.
  • auto newCallable = bind(callable, arg_list);
  • Parameters in arglist may include names like _n, where n is an integer. The number n indicates the position of the parameter in the generated callable object: _1 is the first parameter of newCallable, _2 is the second parameter, and so on.

...

C++ Features#

  • unique_ptr<Type>

Represents a non-shared pointer that cannot be copied and can only be moved (std::move), created using make_unique<Type>(parameters). It belongs to the header file <memory> and is part of the C++ standard library.

Refer to How to: Create and use unique_ptr instances

  • inline

A keyword indicating inline. During the compilation process, calls to inline code segments are directly replaced. It only applies to simple functions and is merely a suggestion to the compiler; it must be meaningful when placed together with the actual implementation of the function body (it is ineffective if only applied to the declaration).

Refer to Usage of inline in C++

  • const

A keyword indicating a constant. The object or variable modified by it cannot be changed.

Const objects must be initialized and are only valid within the file.

If you want to share const objects across multiple files, you must add the extern keyword before the variable definition.

A constant reference can bind to a non-constant object, but the non-constant object cannot be changed through the constant reference. Similarly, a constant pointer can bind to a non-constant object.

Refer to const (C++)

  • constexpr

Used to modify compiler constants. The compiler verifies whether the value of the variable is a constant expression.

In C++11, constexpr indicates "constant," while const indicates "read-only."

When modifying pointers, constexpr only applies to the pointer itself and not to the object pointed to (it defines the object as top-level const).

  • memcpy

memcpy(a,b,c) copies c bytes from b to a.

It belongs to the standard library cstring.

  • override

The override keyword is used after functions in derived classes that need to override; if these functions are not overridden, the compiler will throw an error.

It prevents directly inheriting the interface and default implementation of base class member functions.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.