2021-09-04 learning record of C++ Primer: Chapters 4, 5 and 6

Posted by jenp on Fri, 17 Sep 2021 00:55:03 +0200

Chapter 4 expressions

4.1 Foundation

4.1.1 basic concepts

Function call is also a special operator, which has no limit on the number of operands.

(1) Left and right values

Simple induction: when an object is used as an R-value, 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.

Where an R-value is required, it can be replaced by an l-value, but the R-value cannot be used as an l-value (that is, position). When an lvalue is used as an lvalue, its content (value) is actually used.

(2) decltype and expression

If the expression evaluates to an lvalue, decltype acts on the expression (not a constant) to get a reference type.

If the evaluation result of an expression is an R-value, decltype acts on the expression to obtain the type of the R-value.

int *p;
decltype (*p) x;	// Equivalent to int & X
decltype (&p) y;	// Equivalent to int **y

4.1.3 evaluation sequence

(1) Operation order of calling the same object

For some operators without specified order, if the expression points to and modifies the same object, it will raise an error and produce undefined behavior. For example,

int i = 0;
cout << i << " " << i++ << endl;	// Undefined


The compiler may first find the value of i + + and then the value of i. at this time, the output result is 1;

It is also possible to find the value of i first and then the value of i + +, and the output result is 0 1;

Even the compiler may do completely different operations, which are wrong.

(2) Function calls the same object

The evaluation order of operands is independent of priority and association law.

In expressions such as f() + g() * h() + j(), their calling order is not clearly defined and unrestricted. If several of these functions affect the same object, it is an incorrect expression and will produce undefined behavior.

4.2 arithmetic operators

In the division operation, if the symbols of two operation objects are the same, the quotient is positive, otherwise the quotient is negative. The early versions of C + + language allowed the quotient with negative result to obtain evidence up or down, and the new C++11 standard stipulated that the quotient should be rounded to 0 (directly cut off the decimal part).

Quotient and remainder algorithm:

1. (- m) / n = m / (- n) = - (m / n)
2. m % (- n) = m % n
3. (- m) % n = - (m % n)
 21 % 6     /* The result is 3 */      21 / 6     /* The result is 3 */
21 % 7     /* The result is 0 */      21 / 7     /* The result is 3 */
-21 % -8    /* The result is - 5 */    -21 / -8    /* The result is 2 */
21 % -5    /* The result is 1 */      21 / -5    /* The result is - 4 */


4.5 increment and decrement operators

Recommendation: do not use the Post version of the increment decrement operation unless necessary.

The increment operator of the preceding version avoids unnecessary work. It returns the changed operand directly after the value + 1. In contrast, the Post version needs to store the original value in order to return the unmodified value. If we don't need this content, it will cause an unnecessary waste.

4.9 sizeof operator

• The sizeof operation is performed on char or an expression of type char, and the result is 1
• The size of the space occupied by the referenced object is obtained by performing sizeof operation on the reference type
• Execute sizeof operation on the pointer to get the space occupied by the pointer itself
• The sizeof operation is performed on the dereference pointer to obtain the space occupied by the object pointed to by the pointer. The pointer does not need to be valid
• sizeof operator can calculate the size of object members without providing a specific object
• The size of the space occupied by the whole array is obtained by performing sizeof operation on the array, which is equivalent to performing sizeof operation on all elements of the array and summing the results. The sizeof operation does not convert an array to a pointer for processing
• The sizeof operation on a string object or vector object only returns the size of the fixed type part, and does not calculate how much space the elements in the object occupy
constexpr size_t sz = sizeof(ia) / size0f(*ia);	// sz is the length of array ia
int arr2[sz];	// correct


4.10 comma operator

The comma operator first evaluates the expression on the left, and then discards the result. The real result is the value of the right expression. If the right operation is an lvalue, the final evaluation result is also an lvalue.

4.11 type conversion

4.11.2 other implicit type conversions

(1) Convert array to pointer

int ia[100];
int *ip = ia;	// ia is converted to a pointer to the first element of the array


(2) Pointer conversion

1. 0 and nullptr are converted to any pointer type
2. Convert pointer to void *, const void*
3. Subclass to parent

(3) Convert to boolean type

char *cp = get_string();
if (cp) /* ... */        // If the pointer cp is not 0, the condition is true
while (*cp) /* ... */    // If * cp is not an empty character, the condition is true


(4) Convert to constant

int i;
const int &j = i;
const int *p = i;


(5) Conversion of class type definition

string s, t = "a value";	// Convert string literal to string type

4.11.3 explicit conversion

The form of cast is:
c a s t - n a m e < t y p e >  ( e x p r e s s i o n ) ; cast\text{-}name\text<type\text>\ (expression); cast-name<type> (expression);
​ cast-name: static_cast,dynamic_cast,const_cast,reinterpret_ One of cast. Where, dynamic_cast supports runtime type identification, which is described in section 19.2.

expression: value to convert

Type: target type of conversion

(1)static_cast

Any well-defined type conversion can use static as long as it does not include the underlying const_ cast.

Function: assign a larger arithmetic type to a smaller type.

The compiler will know that we don't care about the loss of precision and will not issue a warning. We can use static_cast finds the value that exists in the void * pointer.

void *p = &d;	// correct. The address of any non constant object can be converted to void*
double *dp = static_cast<double*> (p);


(2)const_cast

Function: it can only change the underlying const of the object. It is often used in the context of function overloading.

const char *pc;
char *p = const_cast<char*> (pc);	// correct. However, writing values through p is an undefined behavior

void func(const int& a)//The formal parameter is int, and the reference points to const int
{
int& b = const_cast<int&>(a);//Remove the const limit because it was originally an extraordinary quantity
b++;
}


The const property of the constant can be removed, and the compiler will not prevent us from writing to the object.

If the object itself is not a constant, it is legal to use cast to obtain write permission;

If the object is a constant, use const_ If cast performs a write operation, it will have undefined consequences.

(3)reinterpret_cast

Function: provides a lower level reinterpretation of the bit pattern of the operand. (not recommended)

int *ip;
char *pc = reinterpret_cast<char*> (ip);


(4) Old style cast

int i = (int) 3.14;
double d = double (3);


Suggestion: avoid cast.

Chapter 5 sentence

5.3 conditional statements

5.3.1 if statement

Matching problem of if and else

if (grade % 10 > 3)
if (grade % 10 > 7)
else
lettergrade += '-';	// Although the indent is the same column as the if of the outer layer, it actually matches the if of the inner layer


Braces should be added as much as possible to identify blocks.

if (grade % 10 > 3)
{
if (grade % 10 > 7)
}
else
lettergrade += '-';	// Match if of outer layer

5.3.2 switch statement

case label must be an integer constant expression.

Defining a default tag is useful even if you are not going to do anything under the default tag. The purpose is to tell the program that the reader has considered the default situation, but has done nothing.

5.4 iteration statement

5.4.3 scope for statement

If you want to write to an element in a sequence, the loop variable must be declared as a reference type.

5.6 try statement block and exception handling

5.6.3 standard exceptions

The exception classes defined in the C + + standard library are in four header files:

1. < exception >: the most common exception class. Only the occurrence of exceptions is reported, and no additional information is provided.

2. < stdecept >: defines several common exception classes.

3. < New >: defines bad_alloc exception type. See section 12.1.2

4. < type_ Info >: bad defined_ Cast exception type. See section 19.2

For exception and bad_alloc and type_info objects can only be created by default initialization, and initial values are not allowed for these objects. Other exception classes need to provide an initial value of a string.

The exception type only defines a what() member function that returns a string that provides text information about the exception.

Chapter 6 functions

6.1 function basis

1. Initialize the formal parameter corresponding to the function with the actual parameter
2. Transfer control to the called function

After the function is executed, two tasks are also completed:

1. Returns the value in return (if any)
2. Transfer control from the called function back to the calling function

The compiler can evaluate arguments in any feasible order.

For compatibility with C language, you can use keywords to indicate that the function has no formal parameters:

void f(void) { /* ... */ }


6.2 parameter transfer

6.2.4 array parameters

(1) Three common techniques for managing pointer parameters

1. Use tag to specify array length

void print(const char *cp)
{
if (cp)
while (*cp)
cout << *cp++;
}

2. Use standard library specifications

void print(const int *beg, const int *end)
{
while (beg != end)
cout << *beg++;
}

3. The display passes a formal parameter representing the size of the array

void print(const int ia[], size_t size)
{
for (size_t i = 0; i < size; ++i)
cout << ia[i];
}


(2) Array reference parameter

Array reference parameters limit the availability of functions: functions can only be applied to arrays of a specified size.

void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem;
}

// &The parentheses at both ends of arr are indispensable
f(int &arr[10]);	// The arr is declared as a referenced array


(3) Pass multidimensional array

void print(int (*matrix)[10], int rowSize) { /* ... */ }

// Equivalent definition
void print(int matrix[][10], int rowSize) { /* ... */ }

6.2.6 functions with variable parameters

(1)initializer_list parameter

​ initializer_list is a standard library type used to represent an array of values of a specific type, which is defined in the header file < initializer_list >.

void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << endl;
for (const auto &elem : il)
cout << elem << " ";
cout << endl;
}

// expected and actual are string objects
if (expected != actual)
error_msg(ErrCode(42), {"functionX", expected, actual});
else
error_msg(ErrCode(0), {"functionX", "okay"});


(2) Ellipsis parameter

The ellipsis parameter is set to facilitate C + + programs to access some special C code, which uses the C standard library function called varargs. Ellipsis parameters should only be used for types common to C and C + +. Most types of objects cannot be copied correctly when passed to ellipsis parameters.

void foo(parm_list, ...);
void foo(...);


The first form specifies the types of some formal parameters of the foo() function, and the arguments corresponding to these formal parameters will perform normal type checking. The argument corresponding to the ellipsis parameter does not need type checking.

6.3 return type and return statement

6.3.2 function with return value

(1) List initialization return value

The new C++11 standard stipulates that functions can return a list of values surrounded by curly braces. The list here is also used to initialize the temporary quantity returned by the function.

vector<string> process()
{
// ...
// expected and actual are string objects
if (expected.empty())
return {};
else if (expected != actual)
return {"functionX", expected, actual};
else
return {"functionX", "okay"};
}


(2) Return value of main function

The return value of the main function can be regarded as a status indicator. Returning 0 means successful execution, and returning other values means failed execution. The specific meaning of non-0 values depends on the machine. In order to make the return value independent of the machine, < cstdlib > header file defines two preprocessing variables, which can be used to represent success and failure respectively.

int main()
{
if (some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}

6.3.3 return array pointer

(1) Use type alias

typedef int arrT[10];	// arrT is a type alias
using arrT = int[10];	// Equivalent declaration of arrT
arrT* func(int i);	// func returns a pointer to an array containing 10 integers


(2) Declare a function that returns an array pointer

int arr[10];
int *p1[10];	// p1 is an array of 10 pointers
int (p2)[10] = &arr;	// p2 is a pointer to an array containing 10 integers


Function form:
T y p e    ( ∗ f u n c t i o n ( p a r a m e t e r _ l i s t ) ) [ d i m e n s i o n ] Type\ \ (*function(parameter\_list))[dimension] Type  (∗function(parameter_list))[dimension]
The following func function declaration does not use a type alias:

int (*func(int i))[10];	// Returns a pointer to an array containing 10 integers


(3) Use trailing return type

In the new C++11 standard, one way to simplify the above func declaration is to use the trailing return type. This form is most effective for functions with complex return types, such as array pointers or array references. In order to indicate that the real return type of the function follows the formal parameter list, we put an auto where the return type should have appeared.

auto func(int i) -> int(*)[10];


(4) Use decltype

If we know which array the pointer returned by the function points to, we can declare the return type using the decltype keyword.

int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};

// Returns a pointer to an array containing 5 integers
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : even;
}


arrPtr uses the keyword decltype to indicate that its return type is a pointer, and the object referred to by the pointer is consistent with the type of odd. Since odd is an array, arrPtr returns a pointer to an array containing five integers.

One thing to note: decltype is not responsible for converting the array type into the corresponding pointer, so the result of decltype is an array. In order to represent the return pointer of arrPtr, you must add a * symbol when declaring the function.

Top level const cannot image the object passed into the function:

Record lookup(Phone);
Record lookup(const Phone);		// Repeat declaration

Record lookup(Phone*);
Record lookup(Phone* const);	// Repeat declaration


The underlying const will distinguish the parameter types:

Record lookup(Account&);
Record lookup(const Account&);	// New function

Record lookup(Account*);
Record lookup(const Account*);	// New function


Recall function shorterString:

const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size ? s1 : s2;
}


We can call this function on two non constant string arguments, but the returned result is still a reference to const string. Therefore, we need a new shorterString function. When its argument is not constant, the result is an ordinary reference, using const_cast can do this:

string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&> (s1), const_cast<const string&> (s2));
return const_cast<const string&> (r);
}


Overloading does not change the general nature of the scope: if we declare a name in the inner scope, it will hide the entities with the same name declared in the outer scope. Function names cannot be overloaded in different scopes:

string read ();
void print(const string s);
void print(double);	// Overload print function
void fooBar (int ival)
{
string s = read();	// Error: read is a Boolean value, not a function
// Bad habit: in general, declaring a function in a local scope is not a good choice

void print (int);	// New scope: the previous print is hidden
print ("Value: ");	// Error: print (const string &) is hidden
print(ival);	// Correct: the current print (int) is visible
print(3.14);	// Correct: call print (int); Print (double) is hidden
}


The process of calling the print function is very similar. The print (int) declared in fooBar hides the first two print functions, so only one print function is available: it takes the int value as an argument.

When we call the print function, the compiler first looks for the declaration of the function name and finds the local declaration that accepts the int value. Once the desired name is found in the current scope, the compiler ignores entities with the same name in the outer scope. The rest of the work is to check whether the function call is valid.

6.5 special purpose language features

6.5.1 default arguments

We use string objects to represent the contents of the window. In general, we want the height, width and background characters of the window to use the default values. However, we should also allow users to freely specify values different from the default values for these parameters. In order to make the window function accept both default values and user specified values, we define it as follows:

typedef string∶∶size_type sz;	// See section 2.5.1 for typedef
string screen(sz ht = 24, sz wid = 80, char backgrnd ='');


We can define default values for one or more formal parameters, but it should be noted that once a formal parameter is given a default value, all subsequent formal parameters must have default values.

(1) Call the function with default arguments

string window;	// Equivalent to screen (24, 80, ")
window = screen();	// Equivalent to screen (66, 80, ")
window = screen(66); window = screen(66,256);	// screen(66,256,'')
window = screen(66, 256, '#');	// screen(66,256,'#')


When a function is called, the argument is parsed according to its position. The default argument is responsible for filling the missing tail argument (on the right) of the function call. For example, to override the default value of backgrnd, you must provide arguments for ht and wid:

window = screen(, , '?');	// Error: only trailing arguments can be omitted
window = screen('?');	// Call screen ('?', 80, '')


It should be noted that the second call passes a character value, which is a legal call. Nevertheless, its actual effect is inconsistent with the intention of writing. The call is legal because ' Is a char, and the type of the leftmost parameter of the function string::size type is an unsigned integer type, so the char type can be converted to the type of the leftmost parameter of the function. When the call occurs, the argument of char type is implicitly converted to string::size type, and then passed to the function as the value of height. On our machine, '?' The corresponding hexadecimal number is 0x3F, that is, 63 of the decimal number, so the call passes the value 63 to the formal parameter height.

When designing functions with default arguments, one of the tasks is to reasonably set the order of formal parameters, try to make formal parameters that do not use the default value appear in the front, and those that often use the default value appear in the back.

(2) Default argument declaration

Note that a formal parameter can only be given a default argument once in a given scope. In other words, the subsequent declaration of a function can only add default arguments to those formal parameters that have no default value before, and all formal parameters to the right of the formal parameter must have default values. If given

// Formal parameters representing height and width have no default values
string screen (sz, sz, char = '');


We cannot modify an existing default value:

string screen (sz, sz, char = '*');	// Error: duplicate declaration


However, you can add default arguments as follows:

string screen (Sz = 24, sz = 80, char);	// Correct: add default argument


(3) Default argument initial value

Local variables cannot be used as default arguments. In addition, as long as the type of the expression can be converted to the type required by the formal parameter, the expression can be used as the default argument:

// Declarations of wd, def, and ht must appear outside the function
sz wd = 80;
char def = ' ';
sz ht();
string screen (sz = ht (),sz = wd,char = def);
string window = screen();	// Call screen(ht(), 80, '')


The names used as default arguments are resolved within the scope of the function declaration, and the evaluation process of these names occurs when the function is called:

void f2()
{
def = '*';	// Change the value of the default argument
sz wd = 100;	// The wd defined by the outer layer is hidden, but the default value is not changed
window = screen ();	// Call screen (ht(), 80, '*')
}


We changed the value of def inside function f2, so the call to screen will pass the updated value. On the other hand, although our function also declares a local variable to hide the outer wd, the local variable has nothing to do with the default argument passed to screen.

6.5.2 inline function and constexpr function

In section 6.4, we wrote a small function shorterString. Its function is to compare the length of two string parameters and return a reference to a string with a smaller length. There are many advantages to define such a small operation as a function, mainly including:

• It is much easier to read and understand the call of the shorterString function than to read the equivalent conditional expression.
• Using functions can ensure the unity of behavior, and each relevant operation can be carried out in the same way.
• If we need to modify the calculation process, it is obvious that modifying the function is better than finding all the occurrences of the equivalent expression first
It's easier to modify them one by one.
• Functions can be reused by other applications, saving programmers the cost of rewriting.

However, there is a potential disadvantage of using the shorterString function: calling the function is generally slower than evaluating the value of the equivalent expression.

On most machines, a function call actually involves a series of work:

• Save the register before calling and restore it on return;
• You may need to copy arguments;
• The program moves to a new location and continues.

(1) Inline function

Specifying a function as an inline function usually means expanding it "inline" at each call point. Suppose we define the shorterString function as an inline function, then call

cout << shorterString(s1, s2) << endl;


Will be expanded during compilation into a form similar to the following

cout << (s1.size() <= s2.size ? s1 : s2;) << endl;


Thus, the running cost of the function is eliminated.

Add the keyword inline before the return type of the shorterString function, so that it can be declared as an inline function:

// Inline version: find the shorter inline const string of the two string objects&
shorterString (const string &s1, const string &s2)
{
return s1.size ()<= s2.size() ? s1 : s2;
}


Note: the inline description is only a request to the compiler, which the compiler can choose to ignore.

(2) constexpr function

The constexpr function follows several conventions:

• The return type of a function and the types of all formal parameters must be literal types
• There must be only one return statement in the function body
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();	// Correct: foo is a constant expression


When this initialization task is performed, the compiler replaces the call to the constexpr function with its result value. In order to expand at any time during compilation, the constexpr function is implicitly specified as an inline function.

The constexpr function body can also contain other statements as long as they do not perform any operation at run time. For example, the constexpr function can have empty statements, type aliases, and using declarations.

The return value of the constexpr function can also not be a constant:

constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }


When the argument of scale is a constant expression, its return value is also a constant expression; Otherwise:

int arr[scale(2)];	// Correct: scale(2) is a constant expression
int i = 2;	// i is not a constant expression
int a2[scale(i)];	// Error: scale(i) is not a constant expression


If we call the scale function with a non constant expression, such as the object i of type int, the return value is a non constant expression. When the scale function is used in a context where a constant expression is required, the compiler is responsible for checking whether the result of the function meets the requirements. If the result happens not to be a constant expression, the compiler issues an error message.

Usually, we put inline functions and constexpr functions in the header file.

6.5.3 commissioning assistance

(1) assert preprocessing macro

Assert is a preprocessor macro. The so-called preprocessing macro is actually a preprocessing variable, and its behavior is somewhat similar to an inline function. The assert macro uses an expression as its condition:

assert(expr);


First evaluate expr. If the expression is false (i.e. 0), assert outputs information and terminates the execution of the program. If the expression is true (that is, not 0), assert does nothing.

The assert macro is defined in the < assert > header file. Preprocessing names are managed by the preprocessor rather than the compiler, so we can use preprocessing names directly without providing a using declaration.

Like preprocessing variables, macro names must be unique within the program. Programs that contain cassert header files can no longer define variables, functions, or other entities named assert. In the actual programming process, even if we do not include the < cassert > header file, it is best not to use assert for other purposes. Many header files contain cassert, which means that even if you don't include cassert directly, it is likely to be included in your program through other ways.

The assert macro is often used to check the "cannot happen" condition. For example, a program that operates on input text may require the length of all given words to exceed a certain threshold. At this time, the program can contain a statement as shown below;

assert(word.size() > threshold);


(2) NDEBUG preprocessing variables

The behavior of assert depends on the state of a preprocessing variable called NDEBUG. If NDEBUG is defined, assert does nothing. NDEBUG is not defined by default, and assert will perform runtime check.

Defining NDEBUG can avoid the runtime overhead of checking various conditions. Of course, runtime checking will not be performed at this time. Therefore, assert should only be used to verify things that are really impossible. We can take assert as an auxiliary means of debugging programs, but it can not replace the real runtime logic check, nor can it replace the error check that the program itself should contain.

In addition to assert, you can also use NDEBUG to write your own conditional debugging code. If NDEBUG is not defined, the code between #ifndef and #endif will be executed; If NDEBUG is defined, these codes will be ignored:

void print (const int ia[],size_t size)
{
#ifndef NDEBUG
//__ func__  Is a local static variable defined by the compiler, which is used to store the name of the function
cerr << __func__ << ": array size is " << size << endl;
#endif
// ...
}


In this code, we use variables__ func__ Output the name of the function currently debugged. The compiler defines for each function__ func__， It is a static array of const char, which is used to store the name of the function.

In addition to C + + compiler defined__ func__ In addition, the preprocessor also defines four other names that are useful for program debugging:

• __ FILE__: The string literal that holds the file name.
• __ LINE__: The integer literal that holds the current line number.
• __ TIME__: The string literal that holds the compilation time of the file.
• __ DATE__: String literal that holds the compilation date of the file.

You can use these constants to provide more information in the error message.

6.6 function matching

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6);			 // Call void f(double, double);
f(42, 2.56);	// Ambiguity occurs and an error is reported


(1) To match functions

1. Step 1 - select the overloaded function set corresponding to this call, and the functions in the set are called candidate functions.

Candidate functions have two characteristics:

• Has the same name as the called function
• Its declaration is visible at the call point.
2. Step 2 - select the functions that can be called by this group of arguments from the candidate functions. These newly selected functions are called feasible functions.

Feasible functions also have two characteristics:

• Its shape parameter quantity is equal to the number of arguments provided by this call
• The type of each argument is the same as the corresponding formal parameter type, or can be converted to the type of formal parameter.
3. Step 3 - select the function that best matches this call from the feasible functions.

In this process, check the arguments provided by the function call one by one to find the feasible function whose formal parameter type best matches the argument type.

In our example, the compiler parses f(5.6) into a call to a function with two double parameters and fills in the second argument we do not provide with the default value.

(2) Function matching with multiple formal parameters

The compiler checks each argument in turn to determine which function is the best match. If only one function satisfies the following conditions, the matching is successful:

• The matching of each argument of the function is not inferior to that required by other feasible functions.
• The matching of at least one argument is better than that provided by other feasible functions.

If no function stands out after checking all the arguments, the call is wrong. The compiler reports information about ambiguous calls.

6.6.1 argument type conversion

In order to determine the best match, the compiler divides the conversion from argument type to formal parameter type into several levels. The specific order is as follows:

1. Exact matching, including the following cases:
• The argument type is the same as the formal parameter type.
• The argument is converted from the array type or function type to the corresponding pointer type
• Add the top-level const to the argument or remove the top-level const from the argument.
2. Matching through const conversion.
3. Matching through type promotion.
4. Matching by arithmetic type conversion or pointer conversion.
5. Matching by class type conversion (see section 14.9, page 514, which describes this conversion in detail).

6.7 function pointer

bool lengthCompare(const string &, const string &);	// Function type
bool (*pf)(const string &, const string &);	// Function pointer


(1) Using function pointers

When a function name is given a function pointer, it is automatically converted to a pointer.

Function pointers can be used directly without dereference.

pf = lengthCompare;		// pf points to a function named lengthCompare
pf = &lengthCompare;	// Equivalent statement

bool b1 = pf("hello", "goodbye");		// Call the lengthCompare function
bool b2 = (*pf)("heelo", "goodbye");	// Equivalent call


The compiler determines which function to use through the pointer type. The pointer type must exactly match one of the overloaded functions.

void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff;		// pf1 points to ff(unsigned)
void (*pf2)(int) = ff;		// Error; No ff matches the parameter list
double (*pf3)(int*) = ff;	// Error; The return types of ff and pf3 do not match


(3) Function pointer as parameter

void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));	// The third parameter is the function type, which is automatically converted to a function pointer

void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));		// Equivalence declaration


Simplify code using type aliases and decltype:

// Func and Func2 are function types
typedef bool Func(const string &, const string &);
typedef decltype (lengthCompare) Func2;		//Equivalent type

// FuncP and FuncP2 are pointers to functions
typedef bool (*FuncP)(const string &, const string &);
typedef decltype (lengthCompare) *FuncP2;		// Equivalent type

// Equivalent declaration of useBigger, where the type alias is used
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);


(4) Return function pointer

Declare a function that returns a function pointer using a type alias:

using F = int(int *, int);		// F is a function type, not a pointer
using PF = int(*)(int *, int);	// PF is a pointer type


It should be noted that, unlike formal parameters of function types, return types are not automatically converted to pointers. We must explicitly specify the return type as a pointer:

PF f1(int);		// Correct: PF is the pointer to the function, and f1 returns the pointer to the function
F f1(int);		// Error: F is a function type, f1 cannot return a function
F *f1(int);		// Correct: explicitly specify that the return type is a pointer to a function


Of course, we can also directly declare f1 in the following form:

int (*f1(int))(int*,int);


Read this declaration from the inside out: we see that f1 has a formal parameter list, so f1 is a function; f1 is preceded by *, so f1 returns a pointer; Further observation shows that the type of the pointer itself also contains a formal parameter list, so the pointer points to a function whose return type is int.

You can also declare a function that returns a function pointer by using a trailing return type:

auto f1(int) -> int (*)(int *, int);


(5) Use auto and decltype for function pointer types

If we know exactly which function is returned, we can use decltype to simplify the process of writing the return type of function pointer.

string::size type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string );
// According to the value of its formal parameter, the getFcn function returns a pointer to sumLength or largerLength
decltype (sumLength) *getFcn(const string &);


The only thing to note about declaring getFcn is to remember that when we apply decltype to a function, it returns the function type instead of the pointer type. Therefore, we explicitly add * to indicate that we need to return the pointer, not the function itself.

Topics: C C++