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...}
orvector<T> v5={a,b,c...}
), copy (vector<T> v2(v1)
orvector<T> v2 = v1
), construction (vector<T> v3(n,val)
orvector<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
andm/(-n)
are equivalent to-(m/n)
;m%(-n)
is equivalent tom%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)
andsizeof 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 toint
,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; onlyconst_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 unmatchedif
; 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 towhile
, except that it executes the loop body first and then checks the condition.
Jump Statements#
break
terminates the nearestwhile
,do while
,for
, orswitch
statement and continues execution from the first statement after these statements.continue
is used to terminate the current iteration of the nearestfor
,while
, ordo 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 morecatch
clauses, with the exception thrown intry
selecting the correspondingcatch
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 aconst 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]
, withargv[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 tovector
. - Elements in
initializer_list
are always constant values, and the values of the elements in theinitializer_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, ...);
andvoid 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 isvector<Type>
). - The header file
cstdlib
defines two preprocessor variablesEXIT_FAILURE
andEXIT_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 equivalentusing arrT = int[10]
, wherearrT* 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 theNDEBUG
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 asbool (*pf)(const string &, const string &);
. pf = lengthCompare;
is equivalent topf = &lengthCompare
.bool b = pf("hello","goodbye");
is equivalent tobool b = (*pf)("hello","goodbye");
andbool 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 astypedef decltype(lengthCompare) Func;
defines the function type, andtypedef decltype(lengthCompare) *FuncP;
defines a pointer to the function type. using F = int(int*, int);
defines the function type F, andusing 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 initializesthis
. - Because the purpose of
this
is always to point to "this" object,this
is a constant pointer, and the address stored inthis
cannot be changed. - When the
const
keyword is placed after the parameter list of a member function, theconst
immediately following the parameter list modifies the type of the implicitthis
pointer, indicating thatthis
is a pointer to const => member functions that useconst
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
orstring
objects to manage the necessary storage space, as usingvector
orstring
can avoid the complexities of allocating and releasing memory. - If a class contains
vector
orstring
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
andstruct
keywords is that their default access permissions are different; either can be used to define a class. struct
: members before the first access specifier arepublic
;class
: members before the first access specifier areprivate
.- 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 addingmutable
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 asthis->member_variable_name
orClassName::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, andexplicit
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 asstatic_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 novirtual
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 aconstexpr
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 asconst
. - 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
, andsstream
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 awhile
loop. - The
rdstate
member of a stream object returns aniostate
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 aflush
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
andcin
are associated with each other. x.tie(&o)
associates stream x with output stream o.- You can associate
istream
withostream
, orostream
withostream
. - 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 anifstream
and opens the given file.- Calling
open
can associate an empty file stream with a file, for example,ofstream out;
andout.open(ofile);
. Useif (out)
to check ifopen
was successful. - To associate a file stream with another file, you must first close the already associated file, such as
in.close()
andin.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 forofstream
) will discard existing data. - To prevent an
ofstream
from clearing the contents of a given file, specify theapp
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
andostringstream
. - 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 tovector
, but specifically for storing characters.string
andvector
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
andforward_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
orforward_list
. - If the program requires random access to elements, use
vector
ordeque
. - If the program requires adding or deleting elements in the middle of the container, use
list
orforward_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'ssort
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 usinglist
during the input phase, and once the input is complete, copy the contents of thelist
tovector
. - If unsure which container to use, you can use only the common operations of
vector
andlist
in the program: use iterators, avoid subscript operations, and avoid random access. This way, it will be convenient to choose to use eithervector
orlist
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
andend
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
andend
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
andcend
should be used. -
Definition and initialization of containers: default constructor, copy initialization
c1(c2)
orc1=c2
, list initializationc{a,b,c...}
orc={a,b,c...}
. Only sequential containers' constructors can accept size parametersseq(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 asarray<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 asswap(c1,c2)
orc1.swap(c2)
. -
assign
(to replace elements) is only applicable to sequential containers and does not support associative containers andarray
, such asseq.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 theswap
operation after theswap
. -
The time required to swap two
array
objects is proportional to the number of elements in thearray
. -
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)
oremplace_back(args)
,push_front(t)
oremplace_front(args)
,insert(p,t)
oremplace(p,args)
, and variousinsert
operations. - Inserting elements into a
vector
,string
, ordeque
will invalidate all iterators, references, and pointers pointing to the container. - Every sequential container except
array
andforward_list
supportspush_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
, anddeque
also supportpush_front
;deque
provides the ability to randomly access elements likevector
, but it offerspush_front
, whichvector
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
, orstring
, 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 toc.push_back(Sales_data("978-0590353403", 25, 15.99))
. emplace
constructs elements directly in the container. The parameters passed toemplace
must match the constructor of the element type.front
andback
return references to the first and last elements, respectively; note the distinction between these andbegin
andend
(the latter are iterators).- Member functions returning references; if the container is a
const
object, the return value is aconst
reference; if the container is notconst
, 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 tostring
,vector
,deque
, andarray
; each sequential container has afront
member function, and all sequential containers exceptforward_list
have aback
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 invector
orstring
will also be invalidated. pop_front
andpop_back
delete the first and last elements, respectively;vector
andstring
do not supportpop_front
, whileforward_list
does not supportpop_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()
andcbefore_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)
; resizingvector
,string
, anddeque
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
, anddeque
. - The program must ensure that iterators, references, or pointers are updated in each loop step, and using
insert
anderase
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
andstring
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()
- reducescapacity()
to be the same size assize()
;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 ofinsert
anderase
(versions that accept indices):s.insert(s.size(), 5, 'i');
,s.erase(s.size()-5, 5);
.string
also providesinsert
andassign
that accept C-style character arrays:s.assign(cp,7);
,s.insert(s.size(),cp+7);
, where cp is aconst char*
.- The
string
class also definesappend
andreplace
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, orstring::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
andfind_last
for reverse searching. - The
compare
function is similar tostrcmp
. - 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 ondeque
for the first two and onvector
for the latter. - Assuming
deq
is adeque<int>
, you can initialize astack
withstack<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 onlist
orvector
. - 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 ondeque
by default, and thepriority_queue
is implemented based onvector
by default. - The
queue
can also be implemented onlist
orvector
, and thepriority_queue
can also be implemented ondeque
. - Other operations for the queue:
q.pop()
returns the first element of thequeue
or the highest priority element of thepriority_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 toqueue
;q.top()
returns the highest priority element but does not delete it, only applicable topriority_queue
;q.push(item)
/q.emplace(args)
creates an element at the end of thequeue
or in the appropriate position in thepriority_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 filenumeric
. - 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()
andcend()
. However, if you plan to use iterators returned by the algorithm to change the values of elements, you need to use the results ofbegin()
andend()
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 initerator
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 returnsvoid
. - 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 infunctional
, 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.