(原) c++ 杂

时间:2022-05-25 23:18:46

Declaration of variables

 

C++ is a strongly-typed language, and requires every variable to be declared with its type before its first use. This informs the compiler the size to reserve in memory for the variable and how to interpret its value. The syntax to declare a new variable in C++ is straightforward: we simply write the type followed by the variable name (i.e., its identifier). For example:

int a;

float mynumber;

 

These are two valid declarations of variables. The first one declares a variable of type int with the identifier a. The second one declares a variable of type float with the identifier mynumber. Once declared, the variables a and mynumber can be used within the rest of their scope in the program.

If declaring more than one variable of the same type, they can all be declared in a single statement by separating their identifiers with commas. For example:

int a, b, c;

 

This declares three variables (a, b and c), all of them of type int, and has exactly the same meaning as:

 

int a;

int b;

int c;

 

To see what variable declarations look like in action within a program, let's have a look at the entire C++ code of the example about your mental memory proposed at the beginning of this chapter:

// operating with variables

 

#include<iostream>

usingnamespace std;

 

int main ()

{

  // declaring variables:

  int a, b;

  int result;

 

  // process:

  a = 5;

  b = 2;

  a = a + 1;

  result = a - b;

 

  // print out the result:

  cout << result;

 

  // terminate the program:

  return 0;

}

 

you should get

 

4

Don't be worried if something else than the variable declarations themselves look a bit strange to you. Most of it will be explained in more detail in coming chapters.

 

 

Initialization of variables

 

When the variables in the example above are declared, they have an undetermined value until they are assigned a value for the first time. But it is possible for a variable to have a specific value from the moment it is declared. This is called the initialization of the variable.

 

In C++, there are three ways to initialize variables. They are all equivalent and are reminiscent of the evolution of the language over the years:

c-like initialization/ constructor initialization/ uniform initialization

 

1. c-like initialization: consists of appending an equal sign followed by the value to which the variable is initialized:

type identifier = initial_value; 

For example, to declare a variable of type int called x and initialize it to a value of zero from the same moment it is declared, we can write:

int x = 0;

 

2. known as constructor initialization:(introduced by the C++ language), encloses the initial value between parentheses (()):

 

type identifier (initial_value); 

For example:

int x (0);

 

3. known as uniform initialization :, but using curly braces ({}) instead of parentheses (this was introduced by the revision of the C++ standard, in 2011):

 

type identifier {initial_value}; 

For example:

int x {0}; 

 

All three ways of initializing variables are valid and equivalent in C++.

// initialization of variables

#include<iostream>

usingnamespace std;

 

int main ()

{

  int a=5;               // initial value: 5

  int b(3);              // initial value: 3

  int c{2};              // initial value: 2

  int result;            // initial value undetermined

 

  a = a + b;

  result = a - c;

  cout << result;

 

  return 0;

}

 

you should get

 

6

 

 

The for loop

 

for (initialization; condition; increase) statement;

 

 

The goto statement

 

goto allows to make an absolute jump to another point in the program. This unconditional jump ignores nesting levels, and does not cause any automatic stack unwinding. Therefore, it is a feature to use with care, and preferably within the same block of statements, especially in the presence of local variables.

The destination point is identified by a label, which is then used as an argument for the goto statement. A label is made of a valid identifier followed by a colon (:).

goto is generally deemed a low-level feature, with no particular use cases in modern higher-level programming paradigms generally used with C++. But, just as an example, here is a version of our countdown loop using goto:

// goto loop example

#include<iostream>

usingnamespace std;

 

int main ()

{

  int n=10;

mylabel:      //这里就是goto 的 jump 之后的位置

  cout << n << ", ";

  n--;

  if (n>0) goto mylabel;

  cout << "liftoff!\n";

}

 

you should get

 

10, 9, 8, 7, 6, 5, 4, 3, 2, 1, liftoff!

 

Arguments passed by value and by reference

  

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;

}

 

 

you should get

 

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: 

                          /‘duplɪket/ adj/n. 完全一样的东西

 

void duplicate (int& a, int& b, int& c)  // PASSED BY REFERENCE 

Was it to be defined without the ampersand signs as:

                                     /'æmpəsænd/

 

void duplicate (int a, int b, int c)  // PASSED BY VALUE

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).

 

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){

 

  cout<<a<<"*";

 

   return (a * factorial (a-1));

 

  }

 

  else{

 

   cout<<1<<"=";

 

  return 1;}

 

}

 

int main ()

 

{

 

  long number = 9;

 

  cout << number << "! = " << factorial (number);

 

  return 0;

 

}

 

you should get

 

9! = 9*8*7*6*5*4*3*2*1=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)

 

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: 

 

#include<iostream>

usingnamespace std;

 

int operate (int a, int b)

{

  return (a*b);

}

 

string operate (string a, string b)

{

  return (a+b);

}

 

int main ()

{

  int x=5,y=2;

  string n="Hello ";

  string m="Wang";

  cout << operate (x,y) << '\n';

  cout << operate (n,m) << '\n';

  return 0;

}

 

you should get 

 

10

 

Hello Wang

 

 

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

          /'tɛmplet/ n. 模板

 

C++ has the ability to define functions with generic types, known as function templates. Defining a function template follows the same syntax than 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 <classSomeType>

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<int> 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 <classT>

T sum (T a, T b)

{

  T result;

  result = a + b;

  return result;

}

 

int main () {

  int i=5, j=6, k;

  string f="HELLO ", g="WANG", h;

  k=sum<int>(i,j);

  h=sum<string>(f,g);

  cout << k << '\n';

  cout << h << '\n';

  return 0;

}

 

 

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. 

with arguments of type int. 

2. with arguments of type string. 

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;

 

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 <classT, classU>

bool are_equal (T a, U b)

{

  return (a==b);

}

 

int main ()

{

  if (are_equal(10,10.0))

    cout << "x and y are equal\n";

  else

    cout << "x and y are not equal\n";

  return 0;

}

 

you should get

 

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)

 

 

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 <classT, intN>

T fixed_multiply (T val)

{

  return val * N;

}

 

int main() {

  cout << fixed_multiply<int,2>(10) << '\n';

  cout << fixed_multiply<int,3>(10) << '\n';

}

 

you should get

 

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).

Namespace

 

A namespace is an optionally named scope. You declare names inside a namespace as you would for a class or an enumeration. You can access names declared inside a namespace the same way you access a nested class name by using the scope resolution (::) operator. However namespaces do not have the additional features of classes or enumerations. The primary purpose of the namespace is to add an additional identifier (the name of the namespace) to a name

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>

usingnamespace std;

 

namespace foo

{

  int value() { return 5; }

}

 

namespace bar

{

  constdouble pi = 3.1416;

  double value() { return 2*pi; }

}

 

int main () {

  cout << foo::value() << '\n';

  cout << bar::value() << '\n';

  cout << bar::pi << '\n';

  return 0;

}

 

you should get

 

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 second::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).

Namespace aliasing

 

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

 

namespace new_name = current_name;

 

Pointers and arrays

mypointer = myarray;

注: 这里= 的顺序非常重要

// more pointers

 

#include<iostream>

 

usingnamespace std;

 

 

int main ()

 

{

 

  int numbers[5];

 

  int * p;

 

  p = numbers;  *p = 10;

 

  p++;  *p = 20;

 

  p = &numbers[2];  *p = 30;

 

  p = numbers + 3;  *p = 40;

 

  p = numbers;  *(p+4) = 50;

 

  for (int n=0; n<5; n++)

 

    cout << numbers[n] << ", ";

 

  return 0;

 

}

 

you should get

 

10, 20, 30, 40, 50,

 

*p++   // same as *(p++): increment pointer, and dereference unincremented address

 

*++p   // same as *(++p): increment pointer, and dereference incremented address

 

++*p   // same as ++(*p): dereference pointer, and increment the value it points to

 

(*p)++ // dereference pointer, and post-increment the value it points to

 

Pointers to pointers

 

 

 

C++ allows the use of pointers that point to pointers, that these, in its turn, point to data (or even to other pointers). The syntax simply requires an asterisk (*) for each level of indirection in the declaration of the pointer:

 

 

 

 

 

char a;

 

char * b;

 

char ** c;

 

a = 'z';

 

b = &a;

 

c = &b;

 

 (原) c++ 杂

 

c is of type char** and a value of 8092

 

*c is of type char* and a value of 7230

 

**c is of type char and a value of 'z'

 

void pointers

 

The void type of pointer is a special type of pointer. In C++, void represents the absence of type. Therefore, void pointers are pointers that point to a value that has no type (and thus also an undetermined length and undetermined dereferencing properties).

 

This gives void pointers a great flexibility, by being able to point to any data type, from an integer value or a float to a string of characters. In exchange, they have a great limitation: the data pointed by them cannot be directly dereferenced (which is logical, since we have no type to dereference to), and for that reason, any address in a void pointer needs to be transformed into some other pointer type that points to a concrete data type before being dereferenced.

 

One of its possible uses may be to pass generic parameters to a function. For example: 

 

 

// increaser

#include<iostream>

usingnamespace std;

 

void increase (void* data, int psize) // void *data 是空指针

{

  if ( psize == sizeof(char) )

  { char* pchar; pchar=(char*)data; ++(*pchar); }// 转化成char 指针

  elseif (psize == sizeof(int) )

  { int* pint; pint=(int*)data; ++(*pint); }  // 转化成int 指针

}

 

int main ()

{

  char a = 'x';

  int b = 1602;

  increase (&a,sizeof(a));

  increase (&b,sizeof(b));

  cout << a << ", " << b << '\n';

  return 0;

}

 

sizeof is an operator integrated in the C++ language that returns the size in bytes of its argument. For non-dynamic data types, this value is a constant. Therefore, for example, sizeof(char) is 1, because char is has always a size of one byte. 

 

Invalid pointers and null pointers

 

In principle, pointers are meant to point to valid addresses, 

the address of a variable 

the address of an element in an array.

But pointers can actually point to any address, including addresses that do not refer to any valid element. Typical examples of this are uninitialized pointers and pointers to nonexistent elements of an array:

 

 

int * p;               // uninitialized pointer (local variable)

 

int myarray[10];

int * q = myarray+20;  // element out of bounds 

 

Neither p nor q point to addresses known to contain a value, but none of the above statements causes an error. In C++, pointers are allowed to take any address value, no matter whether there actually is something at that address or not. What can cause an error is to dereference such a pointer (i.e., actually accessing the value they point to). Accessing such a pointer causes undefined behavior, ranging from an error during runtime to accessing some random value.

 

But, sometimes, a pointer really needs to explicitly point to nowhere, and not just an invalid address. For such cases, there exists a special value that any pointer type can take: the null pointer value.

 

 This value can be expressed in C++ in two ways:

either with an integer value of zero

the nullptr keyword:

null

 

int * p = 0;

int * q = nullptr;

int * r = NULL; //defined as an alias of some null pointer constant value (such as 0 or nullptr).

 

注: The difference between the “Null pointer” and “void pointer”

A null pointer is a value that any pointer can take to represent that it is pointing to "nowhere"

while a void pointer is a type of pointer that can point to somewhere without a specific type.

2.1 one refers to the value stored in the pointer
2.2 the other to the type of data it points to.

Pointers to functions

 

The typical use of this is for passing a function as an argument to another function. Pointers to functions are declared with the same syntax as a regular function declaration, except that the name of the function is enclosed between parentheses () and an asterisk (*) is inserted before the name:

 

// pointer to functions

#include<iostream>

usingnamespace std;

 

int addition (int a, int b)

{ return (a+b); }

 

int subtraction (int a, int b)

{ return (a-b); }

 

int operation (int x, int y, int (*functocall)(int,int))   //Pointers to functions

{

  int g;

  g = (*functocall)(x,y);

  return (g);

}

 

int main ()

{

  int m,n;

  int (*minus)(int,int) = subtraction;

 

  m = operation (7, 5, addition);

  n = operation (20, m, minus);

  cout <<n;

  return 0;

}

 

you should get

 

8

 

Type aliases (typedef / using)

 

 

 

In C++, there are two syntaxes for creating such type aliases: 

 

using the typedef keyword:

 

 

 

typedef existing_type new_type_name ;

 

 

 

typedefcharC;

 

typedefunsignedintWORD;

 

typedefchar * pChar;

 

typedefcharfield [50]; 

 

 

 

This defines four type aliases: C, WORD, pChar, and field as char, unsigned int, char* and char[50], respectively. Once these aliases are defined, they can be used in any declaration just like any other valid type:

 

 

 

C mychar, anotherchar, *ptc1;

 

WORD myword;

 

pChar ptc2;

 

field name;

 

 

 

2. using new_type_name = existing_type ;

 

 

 

usingC = char;

 

usingWORD = unsignedint;

 

usingpChar = char *;

 

usingfield = char [50]; 

 

 

 

The only difference being that 

 

typedef has certain limitationsin the realm of templates that using has not. 

 

using is more generic, although typedef has a longer history and is probably more common in existing code.