zoukankan      html  css  js  c++  java
  • C++之函数与模板

    C++的函数一般是由返回类型,参数和语句组合而成;模板的出现是为了决解同样的操作在不同类型数据之间具有通用性。

    Functions

    Functions allow to structure programs in segments of code to perform individual tasks.

    In C++, a function is a group of statements that is given a name, and which can be called from some point of the program. The most common syntax to define a function is:

    type name ( parameter1, parameter2, ...) { statements }
    

    Where:

    • type is the type of the value returned by the function.
    • name is the identifier by which the function can be called.
    • parameters (as many as needed): Each parameter consists of a type followed by an identifier, with each parameter being separated from the next by a comma. Each parameter looks very much like a regular variable declaration (for example: int x), and in fact acts within the function as a regular variable which is local to the function. The purpose of parameters is to allow passing arguments to the function from the location where it is called from.
    • statements is the function's body. It is a block of statements surrounded by braces { } that specify what the function actually does.

    Let's have a look at an example:

    // function example
    #include <iostream>
    using namespace std;
    
    int addition (int a, int b)
    {
      int r;
      r=a+b;
      return r;
    }
    
    int main ()
    {
      int z;
      z = addition (5,3);
      cout << "The result is " << z;
    }
    //The result is 8
    

    This program is divided in two functions: addition and main. Remember that no matter the order in which they are defined, a C++ program always starts by calling main. In fact, main is the only function called automatically, and the code in any other function is only executed if its function is called from main (directly or indirectly).

    In the example above, main begins by declaring the variable z of type int, and right after that, it performs the first function call: it calls addition. The call to a function follows a structure very similar to its declaration. In the example above, the call to addition can be compared to its definition just a few lines earlier:

    The parameters in the function declaration have a clear correspondence to the arguments passed in the function call. The call passes two values, 5 and 3, to the function; these correspond to the parameters a and b, declared for function addition.

    At the point at which the function is called from within main, the control is passed to function addition: here, execution of main is stopped, and will only resume once the addition function ends. At the moment of the function call, the value of both arguments (5 and 3) are copied to the local variables int a and int b within the function.

    Then, inside addition, another local variable is declared (int r), and by means of the expression r=a+b, the result of a plus b is assigned to r; which, for this case, where a is 5 and b is 3, means that 8 is assigned to r.

    The final statement within the function:

    return r;
    

    Ends function addition, and returns the control back to the point where the function was called; in this case: to function main. At this precise moment, the program resumes its course on main returning exactly at the same point at which it was interrupted by the call to addition. But additionally, because addition has a return type, the call is evaluated as having a value, and this value is the value specified in the return statement that ended addition: in this particular case, the value of the local variable r, which at the moment of the return statement had a value of 8.

    Therefore, the call to addition is an expression with the value returned by the function, and in this case, that value, 8, is assigned to z. It is as if the entire function call (addition(5,3)) was replaced by the value it returns (i.e., 8).

    Then main simply prints this value by calling:

    cout << "The result is " << z;
    

    A function can actually be called multiple times within a program, and its argument is naturally not limited just to literals:

    // function example
    #include <iostream>
    using namespace std;
    
    int subtraction (int a, int b)
    {
      int r;
      r=a-b;
      return r;
    }
    
    int main ()
    {
      int x=5, y=3, z;
      z = subtraction (7,2);
      cout << "The first result is " << z << '
    ';
      cout << "The second result is " << subtraction (7,2) << '
    ';
      cout << "The third result is " << subtraction (x,y) << '
    ';
      z= 4 + subtraction (x,y);
      cout << "The fourth result is " << z << '
    ';
    }
    //The first result is 5
    //The second result is 5
    //The third result is 2
    //The fourth result is 6
    

    Similar to the addition function in the previous example, this example defines a subtract function, that simply returns the difference between its two parameters. This time, main calls this function several times, demonstrating more possible ways in which a function can be called.

    Let's examine each of these calls, bearing in mind that each function call is itself an expression that is evaluated as the value it returns. Again, you can think of it as if the function call was itself replaced by the returned value:

    z = subtraction (7,2);
    cout << "The first result is " << z;
    

    If we replace the function call by the value it returns (i.e., 5), we would have:

    z = 5;
    cout << "The first result is " << z;
    

    With the same procedure, we could interpret:

    cout << "The second result is " << subtraction (7,2);
    

    as:

    cout << "The second result is " << 5;
    

    since 5 is the value returned by subtraction (7,2).
    In the case of:

    cout << "The third result is " << subtraction (x,y);
    

    The arguments passed to subtraction are variables instead of literals. That is also valid, and works fine. The function is called with the values x and y have at the moment of the call: 5 and 3 respectively, returning 2 as result.

    The fourth call is again similar:

    z = 4 + subtraction (x,y);
    

    The only addition being that now the function call is also an operand of an addition operation. Again, the result is the same as if the function call was replaced by its result: 6. Note, that thanks to the commutative property of additions, the above can also be written as:

    z = subtraction (x,y) + 4;
    

    With exactly the same result. Note also that the semicolon does not necessarily go after the function call, but, as always, at the end of the whole statement. Again, the logic behind may be easily seen again by replacing the function calls by their returned value:

    z = 4 + 2;    // same as z = 4 + subtraction (x,y);
    z = 2 + 4;    // same as z = subtraction (x,y) + 4; 
    

    Functions with no type. The use of void

    The syntax shown above for functions:

    type name ( argument1, argument2 ...) { statements }
    

    Requires the declaration to begin with a type. This is the type of the value returned by the function. But what if the function does not need to return a value? In this case, the type to be used is void, which is a special type to represent the absence of value. For example, a function that simply prints a message may not need to return any value:

    // void function example
    #include <iostream>
    using namespace std;
    
    void printmessage ()
    {
      cout << "I'm a function!";
    }
    
    int main ()
    {
      printmessage ();
    }
    //I'm a function!
    

    void can also be used in the function's parameter list to explicitly specify that the function takes no actual parameters when called. For example, printmessage could have been declared as:

    void printmessage (void)
    {
      cout << "I'm a function!";
    }
    

    In C++, an empty parameter list can be used instead of void with same meaning, but the use of void in the argument list was popularized by the C language, where this is a requirement.

    Something that in no case is optional are the parentheses that follow the function name, neither in its declaration nor when calling it. And even when the function takes no parameters, at least an empty pair of parentheses shall always be appended to the function name. See how printmessage was called in an earlier example:

    printmessage ();
    

    The parentheses are what differentiate functions from other kinds of declarations or statements. The following would not call the function:

    printmessage;
    

    The return value of main

    You may have noticed that the return type of main is int, but most examples in this and earlier chapters did not actually return any value from main.

    Well, there is a catch: If the execution of main ends normally without encountering a return statement the compiler assumes the function ends with an implicit return statement:

    return 0;
    

    Note that this only applies to function main for historical reasons. All other functions with a return type shall end with a proper return statement that includes a return value, even if this is never used.

    When main returns zero (either implicitly or explicitly), it is interpreted by the environment as that the program ended successfully. Other values may be returned by main, and some environments give access to that value to the caller in some way, although this behavior is not required nor necessarily portable between platforms. The values for main that are guaranteed to be interpreted in the same way on all platforms are:

    valuedescription
    0The program was successful
    EXIT_SUCCESSThe program was successful (same as above).
    This value is defined in header <cstdlib>.
    EXIT_FAILUREThe program failed.
    This value is defined in header <cstdlib>.

    Because the implicit return 0; statement for main is a tricky exception, some authors consider it good practice to explicitly write the statement.

    Arguments passed by value and by reference

    In the functions seen earlier, arguments have always been passed by value. This means that, when calling a function, what is passed to the function are the values of these arguments on the moment of the call, which are copied into the variables represented by the function parameters. For example, take:

    int x=5, y=3, z;
    z = addition ( x, y );
    

    In this case, function addition is passed 5 and 3, which are copies of the values of x and y, respectively. These values (5 and 3) are used to initialize the variables set as parameters in the function's definition, but any modification of these variables within the function has no effect on the values of the variables x and y outside it, because x and y were themselves not passed to the function on the call, but only copies of their values at that moment.

    In certain cases, though, it may be useful to access an external variable from within a function. To do that, arguments can be passed by reference, instead of by value. For example, the function duplicate in this code duplicates the value of its three arguments, causing the variables used as arguments to actually be modified by the call:

    // passing parameters by reference
    #include <iostream>
    using namespace std;
    
    void duplicate (int& a, int& b, int& c)
    {
      a*=2;
      b*=2;
      c*=2;
    }
    
    int main ()
    {
      int x=1, y=3, z=7;
      duplicate (x, y, z);
      cout << "x=" << x << ", y=" << y << ", z=" << z;
      return 0;
    }
    //x=2, y=6, z=14
    

    To gain access to its arguments, the function declares its parameters as references. In C++, references are indicated with an ampersand (&) following the parameter type, as in the parameters taken by duplicate in the example above.

    When a variable is passed by reference, what is passed is no longer a copy, but the variable itself, the variable identified by the function parameter, becomes somehow associated with the argument passed to the function, and any modification on their corresponding local variables within the function are reflected in the variables passed as arguments in the call.

    In fact, a, b, and c become aliases of the arguments passed on the function call (x, y, and z) and any change on a within the function is actually modifying variable x outside the function. Any change on b modifies y, and any change on c modifies z. That is why when, in the example, function duplicate modifies the values of variables a, b, and c, the values of x, y, and z are affected.

    If instead of defining duplicate as:

    void duplicate (int& a, int& b, int& c) 
    

    Was it to be defined without the ampersand signs as:

    void duplicate (int a, int b, int c)
    

    The variables would not be passed by reference, but by value, creating instead copies of their values. In this case, the output of the program would have been the values of x, y, and z without being modified (i.e., 1, 3, and 7).

    Efficiency considerations and const references

    Calling a function with parameters taken by value causes copies of the values to be made. This is a relatively inexpensive operation for fundamental types such as int, but if the parameter is of a large compound type, it may result on certain overhead. For example, consider the following function:

    string concatenate (string a, string b)
    {
      return a+b;
    }
    

    This function takes two strings as parameters (by value), and returns the result of concatenating them. By passing the arguments by value, the function forces a and b to be copies of the arguments passed to the function when it is called. And if these are long strings, it may mean copying large quantities of data just for the function call.

    But this copy can be avoided altogether if both parameters are made references:

    string concatenate (string& a, string& b)
    {
      return a+b;
    }
    

    Arguments by reference do not require a copy. The function operates directly on (aliases of) the strings passed as arguments, and, at most, it might mean the transfer of certain pointers to the function. In this regard, the version of concatenate taking references is more efficient than the version taking values, since it does not need to copy expensive-to-copy strings.

    On the flip side, functions with reference parameters are generally perceived as functions that modify the arguments passed, because that is why reference parameters are actually for.

    The solution is for the function to guarantee that its reference parameters are not going to be modified by this function. This can be done by qualifying the parameters as constant:

    string concatenate (const string& a, const string& b)
    {
      return a+b;
    }
    

    By qualifying them as const, the function is forbidden to modify the values of neither a nor b, but can actually access their values as references (aliases of the arguments), without having to make actual copies of the strings.

    Therefore, const references provide functionality similar to passing arguments by value, but with an increased efficiency for parameters of large types. That is why they are extremely popular in C++ for arguments of compound types. Note though, that for most fundamental types, there is no noticeable difference in efficiency, and in some cases, const references may even be less efficient!

    Inline functions

    Calling a function generally causes a certain overhead (stacking arguments, jumps, etc...), and thus for very short functions, it may be more efficient to simply insert the code of the function where it is called, instead of performing the process of formally calling a function.

    Preceding a function declaration with the inline specifier informs the compiler that inline expansion is preferred over the usual function call mechanism for a specific function. This does not change at all the behavior of a function, but is merely used to suggest the compiler that the code generated by the function body shall be inserted at each point the function is called, instead of being invoked with a regular function call.

    For example, the concatenate function above may be declared inline as:

    inline string concatenate (const string& a, const string& b)
    {
      return a+b;
    }
    

    This informs the compiler that when concatenate is called, the program prefers the function to be expanded inline, instead of performing a regular call. inline is only specified in the function declaration, not when it is called.

    Note that most compilers already optimize code to generate inline functions when they see an opportunity to improve efficiency, even if not explicitly marked with the inline specifier. Therefore, this specifier merely indicates the compiler that inline is preferred for this function, although the compiler is free to not inline it, and optimize otherwise. In C++, optimization is a task delegated to the compiler, which is free to generate any code for as long as the resulting behavior is the one specified by the code.

    Default values in parameters

    In C++, functions can also have optional parameters, for which no arguments are required in the call, in such a way that, for example, a function with three parameters may be called with only two. For this, the function shall include a default value for its last parameter, which is used by the function when called with fewer arguments. For example:

    // default values in functions
    #include <iostream>
    using namespace std;
    
    int divide (int a, int b=2)
    {
      int r;
      r=a/b;
      return (r);
    }
    
    int main ()
    {
      cout << divide (12) << '
    ';
      cout << divide (20,4) << '
    ';
      return 0;
    }
    //6
    //5
    

    In this example, there are two calls to function divide. In the first one:

    divide (12)
    

    The call only passes one argument to the function, even though the function has two parameters. In this case, the function assumes the second parameter to be 2 (notice the function definition, which declares its second parameter as int b=2). Therefore, the result is 6.

    In the second call:

    divide (20,4)
    

    The call passes two arguments to the function. Therefore, the default value for b (int b=2) is ignored, and b takes the value passed as argument, that is 4, yielding a result of 5.

    Declaring functions

    In C++, identifiers can only be used in expressions once they have been declared. For example, some variable x cannot be used before being declared with a statement, such as:

    int x;
    

    The same applies to functions. Functions cannot be called before they are declared. That is why, in all the previous examples of functions, the functions were always defined before the main function, which is the function from where the other functions were called. If main were defined before the other functions, this would break the rule that functions shall be declared before being used, and thus would not compile.

    The prototype of a function can be declared without actually defining the function completely, giving just enough details to allow the types involved in a function call to be known. Naturally, the function shall be defined somewhere else, like later in the code. But at least, once declared like this, it can already be called.

    The declaration shall include all types involved (the return type and the type of its arguments), using the same syntax as used in the definition of the function, but replacing the body of the function (the block of statements) with an ending semicolon.

    The parameter list does not need to include the parameter names, but only their types. Parameter names can nevertheless be specified, but they are optional, and do not need to necessarily match those in the function definition. For example, a function called protofunction with two int parameters can be declared with either of these statements:

    int protofunction (int first, int second);
    int protofunction (int, int);
    
    

    Anyway, including a name for each parameter always improves legibility of the declaration.

    // declaring functions prototypes
    #include <iostream>
    using namespace std;
    
    void odd (int x);
    void even (int x);
    
    int main()
    {
      int i;
      do {
        cout << "Please, enter number (0 to exit): ";
        cin >> i;
        odd (i);
      } while (i!=0);
      return 0;
    }
    
    void odd (int x)
    {
      if ((x%2)!=0) cout << "It is odd.
    ";
      else even (x);
    }
    
    void even (int x)
    {
      if ((x%2)==0) cout << "It is even.
    ";
      else odd (x);
    }
    //Please, enter number (0 to exit): 9
    //It is odd.
    //Please, enter number (0 to exit): 6
    //It is even.
    //Please, enter number (0 to exit): 1030
    //It is even.
    //Please, enter number (0 to exit): 0
    //It is even.
    

    This example is indeed not an example of efficiency. You can probably write yourself a version of this program with half the lines of code. Anyway, this example illustrates how functions can be declared before its definition:

    The following lines:

    
    void odd (int a);
    void even (int a);
    
    

    Declare the prototype of the functions. They already contain all what is necessary to call them, their name, the types of their argument, and their return type (void in this case). With these prototype declarations in place, they can be called before they are entirely defined, allowing for example, to place the function from where they are called (main) before the actual definition of these functions.

    But declaring functions before being defined is not only useful to reorganize the order of functions within the code. In some cases, such as in this particular case, at least one of the declarations is required, because odd and even are mutually called; there is a call to even in odd and a call to odd in even. And, therefore, there is no way to structure the code so that odd is defined before even, and even before odd.

    Recursivity

    Recursivity is the property that functions have to be called by themselves. It is useful for some tasks, such as sorting elements, or calculating the factorial of numbers. For example, in order to obtain the factorial of a number (n!) the mathematical formula would be:

    n! = n * (n-1) * (n-2) * (n-3) ... * 1
    More concretely, 5! (factorial of 5) would be:

    5! = 5 * 4 * 3 * 2 * 1 = 120
    And a recursive function to calculate this in C++ could be:

    // factorial calculator
    #include <iostream>
    using namespace std;
    
    long factorial (long a)
    {
      if (a > 1)
       return (a * factorial (a-1));
      else
       return 1;
    }
    
    int main ()
    {
      long number = 9;
      cout << number << "! = " << factorial (number);
      return 0;
    }
    //9! = 362880
    

    Notice how in function factorial we included a call to itself, but only if the argument passed was greater than 1, since, otherwise, the function would perform an infinite recursive loop, in which once it arrived to 0, it would continue multiplying by all the negative numbers (probably provoking a stack overflow at some point during runtime).


    Overloads and templates

    Overloaded functions

    In C++, two different functions can have the same name if their parameters are different; either because they have a different number of parameters, or because any of their parameters are of a different type. For example:

    // overloading functions
    #include <iostream>
    using namespace std;
    
    int operate (int a, int b)
    {
      return (a*b);
    }
    
    double operate (double a, double b)
    {
      return (a/b);
    }
    
    int main ()
    {
      int x=5,y=2;
      double n=5.0,m=2.0;
      cout << operate (x,y) << '
    ';
      cout << operate (n,m) << '
    ';
      return 0;
    }
    //10
    //2.5
    

    In this example, there are two functions called operate, but one of them has two parameters of type int, while the other has them of type double. The compiler knows which one to call in each case by examining the types passed as arguments when the function is called. If it is called with two int arguments, it calls to the function that has two int parameters, and if it is called with two doubles, it calls the one with two doubles.

    In this example, both functions have quite different behaviors, the int version multiplies its arguments, while the double version divides them. This is generally not a good idea. Two functions with the same name are generally expected to have -at least- a similar behavior, but this example demonstrates that is entirely possible for them not to. Two overloaded functions (i.e., two functions with the same name) have entirely different definitions; they are, for all purposes, different functions, that only happen to have the same name.

    Note that a function cannot be overloaded only by its return type. At least one of its parameters must have a different type.

    Function templates

    Overloaded functions may have the same definition. For example:

    // overloaded functions
    #include <iostream>
    using namespace std;
    
    int sum (int a, int b)
    {
      return a+b;
    }
    
    double sum (double a, double b)
    {
      return a+b;
    }
    
    int main ()
    {
      cout << sum (10,20) << '
    ';
      cout << sum (1.0,1.5) << '
    ';
      return 0;
    }
    //30
    //2.5
    

    Here, sum is overloaded with different parameter types, but with the exact same body.

    The function sum could be overloaded for a lot of types, and it could make sense for all of them to have the same body. For cases such as this, C++ has the ability to define functions with generic types, known as function templates. Defining a function template follows the same syntax as a regular function, except that it is preceded by the template keyword and a series of template parameters enclosed in angle-brackets <>:

    template <template-parameters> function-declaration

    The template parameters are a series of parameters separated by commas. These parameters can be generic template types by specifying either the class or typename keyword followed by an identifier. This identifier can then be used in the function declaration as if it was a regular type. For example, a generic sum function could be defined as:

    template <class SomeType>
    SomeType sum (SomeType a, SomeType b)
    {
      return a+b;
    }
    

    It makes no difference whether the generic type is specified with keyword class or keyword typename in the template argument list (they are 100% synonyms in template declarations).

    In the code above, declaring SomeType (a generic type within the template parameters enclosed in angle-brackets) allows SomeType to be used anywhere in the function definition, just as any other type; it can be used as the type for parameters, as return type, or to declare new variables of this type. In all cases, it represents a generic type that will be determined on the moment the template is instantiated.

    Instantiating a template is applying the template to create a function using particular types or values for its template parameters. This is done by calling the function template, with the same syntax as calling a regular function, but specifying the template arguments enclosed in angle brackets:

    name <template-arguments> (function-arguments)
    

    For example, the sum function template defined above can be called with:

    x = sum<int>(10,20);
    

    The function sum is just one of the possible instantiations of function template sum. In this case, by using int as template argument in the call, the compiler automatically instantiates a version of sum where each occurrence of SomeType is replaced by int, as if it was defined as:

    int sum (int a, int b)
    {
      return a+b;
    }
    

    Let's see an actual example:

    // function template
    #include <iostream>
    using namespace std;
    
    template <class T>
    T sum (T a, T b)
    {
      T result;
      result = a + b;
      return result;
    }
    
    int main () {
      int i=5, j=6, k;
      double f=2.0, g=0.5, h;
      k=sum<int>(i,j);
      h=sum<double>(f,g);
      cout << k << '
    ';
      cout << h << '
    ';
      return 0;
    }
    //11
    //2.5
    

    In this case, we have used T as the template parameter name, instead of SomeType. It makes no difference, and T is actually a quite common template parameter name for generic types.

    In the example above, we used the function template sum twice. The first time with arguments of type int, and the second one with arguments of type double. The compiler has instantiated and then called each time the appropriate version of the function.

    Note also how T is also used to declare a local variable of that (generic) type within sum:

    T result;
    

    Therefore, result will be a variable of the same type as the parameters a and b, and as the type returned by the function.
    In this specific case where the generic type T is used as a parameter for sum, the compiler is even able to deduce the data type automatically without having to explicitly specify it within angle brackets. Therefore, instead of explicitly specifying the template arguments with:

    k = sum<int> (i,j);
    h = sum<double> (f,g);
    

    It is possible to instead simply write:

    k = sum (i,j);
    h = sum (f,g);
    

    without the type enclosed in angle brackets. Naturally, for that, the type shall be unambiguous. If sum is called with arguments of different types, the compiler may not be able to deduce the type of T automatically.

    Templates are a powerful and versatile feature. They can have multiple template parameters, and the function can still use regular non-templated types. For example:

    // function templates
    #include <iostream>
    using namespace std;
    
    template <class T, class U>
    bool are_equal (T a, U b)
    {
      return (a==b);
    }
    
    int main ()
    {
      if (are_equal(10,10.0))
        cout << "x and y are equal
    ";
      else
        cout << "x and y are not equal
    ";
      return 0;
    }
    //x and y are equal
    

    Note that this example uses automatic template parameter deduction in the call to are_equal:

    are_equal(10,10.0)
    

    Is equivalent to:

    are_equal<int,double>(10,10.0)
    

    There is no ambiguity possible because numerical literals are always of a specific type: Unless otherwise specified with a suffix, integer literals always produce values of type int, and floating-point literals always produce values of type double. Therefore 10 has always type int and 10.0 has always type double.

    Non-type template arguments

    The template parameters can not only include types introduced by class or typename, but can also include expressions of a particular type:

    // template arguments
    #include <iostream>
    using namespace std;
    
    template <class T, int N>
    T fixed_multiply (T val)
    {
      return val * N;
    }
    
    int main() {
      std::cout << fixed_multiply<int,2>(10) << '
    ';
      std::cout << fixed_multiply<int,3>(10) << '
    ';
    }
    //20
    //30
    

    The second argument of the fixed_multiply function template is of type int. It just looks like a regular function parameter, and can actually be used just like one.

    But there exists a major difference: the value of template parameters is determined on compile-time to generate a different instantiation of the function fixed_multiply, and thus the value of that argument is never passed during runtime: The two calls to fixed_multiply in main essentially call two versions of the function: one that always multiplies by two, and one that always multiplies by three. For that same reason, the second template argument needs to be a constant expression (it cannot be passed a variable).


    Name visibility

    Scopes

    Named entities, such as variables, functions, and compound types need to be declared before being used in C++. The point in the program where this declaration happens influences its visibility:

    An entity declared outside any block has global scope, meaning that its name is valid anywhere in the code. While an entity declared within a block, such as a function or a selective statement, has block scope, and is only visible within the specific block in which it is declared, but not outside it.

    Variables with block scope are known as local variables.

    For example, a variable declared in the body of a function is a local variable that extends until the end of the the function (i.e., until the brace } that closes the function definition), but not outside it:

    int foo;        // global variable
    int some_function ()
    {
      int bar;      // local variable
      bar = 0;
    }
    
    int other_function ()
    {
      foo = 1;  // ok: foo is a global variable
      bar = 2;  // wrong: bar is not visible from this function
    }
    

    In each scope, a name can only represent one entity. For example, there cannot be two variables with the same name in the same scope:

    int some_function ()
    {
      int x;
      x = 0;
      double x;   // wrong: name already used in this scope
      x = 0.0;
    }
    

    The visibility of an entity with block scope extends until the end of the block, including inner blocks. Nevertheless, an inner block, because it is a different block, can re-utilize a name existing in an outer scope to refer to a different entity; in this case, the name will refer to a different entity only within the inner block, hiding the entity it names outside. While outside it, it will still refer to the original entity. For example:

    // inner block scopes
    #include <iostream>
    using namespace std;
    
    int main () {
      int x = 10;
      int y = 20;
      {
        int x;   // ok, inner scope.
        x = 50;  // sets value to inner x
        y = 50;  // sets value to (outer) y
        cout << "inner block:
    ";
        cout << "x: " << x << '
    ';
        cout << "y: " << y << '
    ';
      }
      cout << "outer block:
    ";
      cout << "x: " << x << '
    ';
      cout << "y: " << y << '
    ';
      return 0;
    }
    
    //inner block:
    //x: 50
    //y: 50
    //outer block:
    //x: 10
    //y: 50
    

    Note that y is not hidden in the inner block, and thus accessing y still accesses the outer variable.

    Variables declared in declarations that introduce a block, such as function parameters and variables declared in loops and conditions (such as those declared on a for or an if) are local to the block they introduce.

    Namespaces

    Only one entity can exist with a particular name in a particular scope. This is seldom a problem for local names, since blocks tend to be relatively short, and names have particular purposes within them, such as naming a counter variable, an argument, etc...

    But non-local names bring more possibilities for name collision, especially considering that libraries may declare many functions, types, and variables, neither of them local in nature, and some of them very generic.

    Namespaces allow us to group named entities that otherwise would have global scope into narrower scopes, giving them namespace scope. This allows organizing the elements of programs into different logical scopes referred to by names.

    The syntax to declare a namespaces is:

    namespace identifier
    {
      named_entities
    }
    

    Where identifier is any valid identifier and named_entities is the set of variables, types and functions that are included within the namespace. For example:

    namespace myNamespace
    {
      int a, b;
    }
    

    In this case, the variables a and b are normal variables declared within a namespace called myNamespace.

    These variables can be accessed from within their namespace normally, with their identifier (either a or b), but if accessed from outside the myNamespace namespace they have to be properly qualified with the scope operator ::. For example, to access the previous variables from outside myNamespace they should be qualified like:

    myNamespace::a
    myNamespace::b
    

    Namespaces are particularly useful to avoid name collisions. For example:

    // namespaces
    #include <iostream>
    using namespace std;
    
    namespace foo
    {
      int value() { return 5; }
    }
    
    namespace bar
    {
      const double pi = 3.1416;
      double value() { return 2*pi; }
    }
    
    int main () {
      cout << foo::value() << '
    ';
      cout << bar::value() << '
    ';
      cout << bar::pi << '
    ';
      return 0;
    }
    //5
    //6.2832
    //3.1416
    

    In this case, there are two functions with the same name: value. One is defined within the namespace foo, and the other one in bar. No redefinition errors happen thanks to namespaces. Notice also how pi is accessed in an unqualified manner from within namespace bar (just as pi), while it is again accessed in main, but here it needs to be qualified as bar::pi.

    Namespaces can be split: Two segments of a code can be declared in the same namespace:

    namespace foo { int a; }
    namespace bar { int b; }
    namespace foo { int c; }
    

    This declares three variables: a and c are in namespace foo, while b is in namespace bar. Namespaces can even extend across different translation units (i.e., across different files of source code).

    using

    The keyword using introduces a name into the current declarative region (such as a block), thus avoiding the need to qualify the name. For example:

    // using
    #include <iostream>
    using namespace std;
    
    namespace first
    {
      int x = 5;
      int y = 10;
    }
    
    namespace second
    {
      double x = 3.1416;
      double y = 2.7183;
    }
    
    int main () {
      using first::x;
      using second::y;
      cout << x << '
    ';
      cout << y << '
    ';
      cout << first::y << '
    ';
      cout << second::x << '
    ';
      return 0;
    }
    //5
    //2.7183
    //10
    //3.1416
    

    Notice how in main, the variable x (without any name qualifier) refers to first::x, whereas y refers to second::y, just as specified by the using declarations. The variables first::y and second::x can still be accessed, but require fully qualified names.

    The keyword using can also be used as a directive to introduce an entire namespace:

    // using
    #include <iostream>
    using namespace std;
    
    namespace first
    {
      int x = 5;
      int y = 10;
    }
    
    namespace second
    {
      double x = 3.1416;
      double y = 2.7183;
    }
    
    int main () {
      using namespace first;
      cout << x << '
    ';
      cout << y << '
    ';
      cout << second::x << '
    ';
      cout << second::y << '
    ';
      return 0;
    }
    //5
    //10
    //3.1416
    //2.7183
    

    In this case, by declaring that we were using namespace first, all direct uses of x and y without name qualifiers were also looked up in namespace first.

    using and using namespace have validity only in the same block in which they are stated or in the entire source code file if they are used directly in the global scope. For example, it would be possible to first use the objects of one namespace and then those of another one by splitting the code in different blocks:

    // using namespace example
    #include <iostream>
    using namespace std;
    
    namespace first
    {
      int x = 5;
    }
    
    namespace second
    {
      double x = 3.1416;
    }
    
    int main () {
      {
        using namespace first;
        cout << x << '
    ';
      }
      {
        using namespace second;
        cout << x << '
    ';
      }
      return 0;
    }
    //5
    //3.1416
    

    Namespace aliasing

    Existing namespaces can be aliased with new names, with the following syntax:

    namespace new_name = current_name;
    

    The std namespace

    All the entities (variables, types, constants, and functions) of the standard C++ library are declared within the std namespace. Most examples in these tutorials, in fact, include the following line:

    using namespace std;
    

    This introduces direct visibility of all the names of the std namespace into the code. This is done in these tutorials to facilitate comprehension and shorten the length of the examples, but many programmers prefer to qualify each of the elements of the standard library used in their programs. For example, instead of:

    cout << "Hello world!";
    

    It is common to instead see:

    std::cout << "Hello world!";
    

    Whether the elements in the std namespace are introduced with using declarations or are fully qualified on every use does not change the behavior or efficiency of the resulting program in any way. It is mostly a matter of style preference, although for projects mixing libraries, explicit qualification tends to be preferred.

    Storage classes

    The storage for variables with global or namespace scope is allocated for the entire duration of the program. This is known as static storage, and it contrasts with the storage for local variables (those declared within a block). These use what is known as automatic storage. The storage for local variables is only available during the block in which they are declared; after that, that same storage may be used for a local variable of some other function, or used otherwise.

    But there is another substantial difference between variables with static storage and variables with automatic storage:

    • Variables with static storage (such as global variables) that are not explicitly initialized are automatically initialized to zeroes.
    • Variables with automatic storage (such as local variables) that are not explicitly initialized are left uninitialized, and thus have an undetermined value.

    For example:

    // static vs automatic storage
    #include <iostream>
    using namespace std;
    
    int x;
    
    int main ()
    {
      int y;
      cout << x << '
    ';
      cout << y << '
    ';
      return 0;
    }
    //0
    //4285838
    

    The actual output may vary, but only the value of x is guaranteed to be zero. y can actually contain just about any value (including zero).

    References

    [1] Functions
    [2] Overloads and templates
    [3] Name visibility

    • 变更记录
    时间 地点 修改人 备注
    2020-09-13 佛山 PatrickLee 首发
  • 相关阅读:
    动态二维码
    二维码
    购物车
    logback学习与配置使用
    UML类图几种关系的总结
    java.lang.Excetion,java.lang.RuntimeException,java.lang.Error有什么区别?
    Java编程最差实践
    Hibernate利用@DynamicInsert和@DynamicUpdate生成动态SQL语句
    从 Java 代码到 Java 堆
    Project configuration is not up-to-date with pom.xml
  • 原文地址:https://www.cnblogs.com/leaguecn/p/13663503.html
Copyright © 2011-2022 走看看