C++ Basic Features - Operators And Statements

Expressions

C++ provides a rich set of operators and defines what these operators do when applied to operands of built-in type. It also allows us to define the meaning of most of the operators when applied to operands of class types. This chapter focuses on the operators as defined in the language and applied to operands of built-in type.

An expression is composed of one or more operands and yields a result when it is evaluated. The simplest form of an expression is a single literal or variable. The result of such an expression is the value of the variable or literal. More complicated expressions are formed from an operator and one or more operands.

Fundamentals

The basic concept of expressions are operator and operands, the operator can be classified by operands, Unary operators, such as address-of (&) and dereference (*), act on one operand. Binary operators, such as equality (==) and multiplication (*), act on two operands. There is also one ternary operator that takes three operands, and one operator, function call, that takes an unlimited number of operands.

Understanding expressions with multiple operators requires understanding the precedence and associativity of the operators and may depend on the order of evaluation of the operands. For example, 5 + 7 * 9, the * has high precedence.

The language defines what the operators mean when applied to built-in and compound types. We can also define what most operators mean when applied to class types. Because such definitions give an alternative meaning to an existing operator symbol, we refer to them as overloaded operators. The IO library >> and << operators and the operators we used with strings, vectors, and iterators are all overloaded operators.

When you write compound expressions, two rules of thumb can be helpful:

  1. When in doubt, parenthesize expressions to force the grouping that the logic of your program requires.
  2. If you change the value of an operand, don’t use that operand elsewhere in the same expression.

Arithmetic Operators

The next table show arithmetic operators in c++ order by precedence.

Operator function expression
+ unary plus + operand1
- unary minus - operand1
* multiplication operand1 * operand2
/ division operand1 / operand2
% reminder operand1 % operand2
+ addition operand1 + operand2
- subtraction operand1 - operand2

Note same operator has different meaning with different type of operand, when division a integer, like 5 / 2, the result is 2 rather than 2.5.

Logical and Relational Operators

The relational operators take operands of arithmetic or pointer type; the logical operators take operands of any type that can be converted to bool. These operators all return values of type bool. Arithmetic and pointer operand(s) with a value of zero are false; all other values are true. The operands to these operators are rvalues and the result is an rvalue. (notice: because my blog convert sheet have error, I replace | symbol with ⏐)

Operator function expression
! logical not ! operand
> greater than operand1 > operand2
>= greater than or equal operand1 >= operand2
< less than operand1 < operand2
<= less than or equal operand1 <= operand2
== equal operand1 == operand2
!= unequal operand1 != operand2
&& logical and operand1 && operand2
⏐⏐ logical or operand1 ⏐⏐ operand2

The Member Access Operators

The dot . and arrow -> operators provide for member access. The dot operator fetches a member from an object of class type; arrow is defined so that ptr->mem is a synonym for (*ptr).mem:

The Conditional Operator

The conditional operator (the ?: operator) lets us embed simple if-else logic inside an expression. The conditional operator has the following form: cond ? expr1 : expr2;. where cond is an expression that is used as a condition and expr1 and expr2 are expressions of the same type (or types that can be converted to a common type). This operator executes by evaluating cond. If the condition is true, then expr1 is evaluated; otherwise, expr2 is evaluated. As one example, we can use a conditional operator to determine whether a grade is pass or fail:

The Bitwise Operators

The bitwise operators take operands of integral type that they use as a collection of bits. These operators let us test and set individual bits. We can also use these operators on a library type named bitset that represents a flexibly sized collection of bits (notice: because my blog convert sheet have error, I replace | symbol with ⏐)

Operator function expression
~ bitwise not ~ operand
>> right shift operand1 >> operand2
<< left shift operand1 << operand2
& bitwise and operand1 & operand2
^ bitwise xor operand1 ^ operand2
bitwise or operand1 ⏐ operand2

The sizeof Operator

The sizeof operator returns the size, in bytes, of an expression or a type name. The operator is right associative. The result of sizeof is a constant expression of type size_t. The operator takes one of two forms: sizeof (type) and sizeof expr.

sizeof(int);
int a = 10;
sizeof a;

Type Explicit Conversions

A named cast has the following form:cast-name<type>(expression); where type is the target type of the conversion, and expression is the value to be cast. If type is a reference, then the result is an lvalue. The cast-name may be one of static_cast, dynamic_cast, const_cast, and reinterpret_cast.

A static_cast is often useful when a larger arithmetic type is assigned to a smaller type. The cast informs both the reader of the program and the compiler that we are aware of and are not concerned about the potential loss of precision.

A static_cast is also useful to perform a conversion that the compiler will not generate automatically. For example, we can use a static_cast to retrieve a pointer value that was stored in a void* pointer:

float a = 1;
void* p = &a;
// *k equal to 1065353216 (IEEE 754)
// *l equal to 1
int *k = static_cast<int*>(p);
float *l = static_cast<float*>(p);

A const_cast changes only a low-level const in its operand:

const int *a;
auto p = const_cast<int*>(a);
*p = 10; // ok
*a = 10; // error

Conventionally we say that a cast that converts a const object to a nonconst type “casts away the const.” Once we have cast away the const of an object, the compiler will no longer prevent us from writing to that object. If the object was originally not a const, using a cast to obtain write access is legal. However, using a const_cast in order to write to a const object is undefined.

Statements

Like most languages, C++ provides statements for conditional execution, loops that repeatedly execute the same body of code, and jump statements that interrupt the flow of control. This chapter looks in detail at the statements supported by C++. The almost statements are same as other language like java, and be used in previous code, so the next only show some new features.

try Blocks and Exception Handling

Exceptions are run-time anomalies—such as losing a database connection or encountering unexpected input—that exist outside the normal functioning of a program. Dealing with anomalous behavior can be one of the most difficult parts of designing any system.

In C++, exception handling involves:

  • throw expressions, which the detecting part uses to indicate that it encountered something it can’t handle. We say that a throw raises an exception.
  • try blocks, which the handling part uses to deal with an exception. A try block starts with the keyword try and ends with one or more catch clauses.
  • A set of exception classes that are used to pass information about what happened between a throw and an associated catch.

Supposed wo want write a program that need read two numbers and then will do division, as we know, zero can’t be divisor. We need throw a arithmetic error if divisor is zero.

#include <iostream>

int main()
{
    int num1, num2;
    while (std::cin >> num1 >> num2){
        if (num2 == 0){
            throw std::runtime_error("divisor can't be zero!");
        }else{
            std::cout << num1 << "/" << num2 << "=" << num1 / num2 << std::endl;
        }
    }
    return 0;
}

Only throw error is no a good idea, we also need catch the error so that program will continued running, and report the error to user:

#include <iostream>

int main()
{
    int num1, num2;
    while (std::cin >> num1 >> num2) {
        try{
            if (num2 == 0) {
                throw std::runtime_error("divisor can't be zero!");
            }
            else {
                std::cout << num1 << "/" << num2 << "=" << num1 / num2 << std::endl;
            }
        }
        catch (std::runtime_error err){
            std::cout << err.what() << std::endl;
            std::cout << "please input a correct divisor" << std::endl;
        }
    }
    return 0;
}

If no appropriate catch is found, execution is transferred to a library function named terminate. The behavior of that function is system dependent but is guaranteed to stop further execution of the program.

Exceptions that occur in programs that do not define any try blocks are handled in the same manner: After all, if there are no try blocks, there can be no handlers. If a program has no try blocks and an exception occurs, then terminate is called and the program is exited.

The C++ library defines several classes that it uses to report problems encountered in the functions in the standard library. These exception classes are also intended to be used in the programs we write. These classes are defined in four headers:

  • The exception header defines the most general kind of exception class named exception. It communicates only that an exception occurred but provides no additional information.
  • The stdexcept header defines several general-purpose exception classes.
  • The new header defines the bad_alloc exception type.
  • The type_info header defines the bad_cast exception type.

Functions And Classes

Function

A function is a block of code with a name. We execute the code by calling the function. A function may take zero or more arguments and (usually) yields a result. Functions can be overloaded, meaning that the same name may refer to several different functions.

Function Basics

A function definition typically consists of a return type, a name, a list of zero or more parameters, and a body. The parameters are specified in a comma-separated list enclosed in parentheses. The actions that the function performs are specified in a statement block, referred to as the function body. example:

int fact(int n) {
    //The factorial of n
    int sum = 1;
    while (n>0)
    {
        sum *= n--;
    }
    return sum;
}

To call fact, we must supply an int value. The result of the call is also an int:

int main()
{
    int f = 0;
    std::cin >> f;
    std::cout << fact(f) << std::endl;
    return 0;
}

A function call does two things:

  • It initializes the function’s parameters from the corresponding arguments.
  • It transfers control to that function. Execution of the calling function is suspended and execution of the called function begins.

Most types can be used as the return type of a function. In particular, the return type can be void, which means that the function does not return a value. However, the return type may not be an array type or a function type. However, a function may return a pointer to an array or a function.

In C++, names have scope, and objects have lifetimes. It is important to understand both of these concepts.

  • The scope of a name is the part of the program’s text in which that name is visible.
  • The lifetime of an object is the time during the program’s execution that the object exists.

Parameters and variables defined inside a function body are referred to as local variables. They are “local” to that function and hide declarations of the same name made in an outer scope.

The objects that correspond to ordinary local variables are created when the function’s control path passes through the variable’s definition. They are destroyed when control passes through the end of the block in which the variable is defined. Objects that exist only while a block is executing are known as automatic objects. After execution exits a block, the values of the automatic objects created in that block are undefined.

It can be useful to have a local variable whose lifetime continues across calls to the function. We obtain such objects by defining a local variable as static. Each local static object is initialized before the first time execution passes through the object’s definition. Local statics are not destroyed when a function ends; they are destroyed when the program terminates. This program will print the numbers from 1 through 10 inclusive:

size_t count_calls(){
    static size_t ctr = 0; // value will persist across calls
    return ++ctr;
}
int main(){
    for (size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

Like any other name, the name of a function must be declared before we can use it. As with variables, a function may be defined only once but may be declared multiple times. A function declaration is just like a function definition except that a declaration has no function body. In a declaration, a semicolon replaces the function body.

Argument Passing

As we’ve seen, each time we call a function, its parameters are created and initialized by the arguments passed in the call. As with any other variable, the type of a parameter determines the interaction between the parameter and its argument. If the parameter is a reference, then the parameter is bound to its argument. Otherwise, the argument’s value is copied.

When a parameter is a reference, we say that its corresponding argument is “passed by reference” or that the function is “called by reference.”When the argument value is copied, the parameter and argument are independent objects. We say such arguments are “passed by value” or alternatively that the function is “called by value.”

#include <iostream>
using std::cin; using std::cout; using std::endl;

void toUper1(char c) {
    c = toupper(c);
}

void toUper2(char &c) {
    c = toupper(c);
}

int main(){
    char c = 'a';
    //no effect, because c in to toUper1 is a copy of c in in to main
    toUper1(c);
    cout << c << endl;// print a
    //effect, because c in to toUper1 is bound to c in in to main
    toUper2(c);
    cout << c << endl;//print A
    return 0;
}

Pointers behave like any other nonreference type. When we copy a pointer, the value of the pointer is copied. After the copy, the two pointers are distinct. However, a pointer also gives us indirect access to the object to which that pointer points. We can change the value of that object by assigning through the pointer:

void toUper3(char *c) {
    *c = toupper(*c);
}
//note in mian wo call this function need pass the address of c
//toUper3(&c)

It can be inefficient to copy objects of large class types or large containers. Moreover, some class types (including the IO types) cannot be copied. Functions must use reference parameters to operate on objects of a type that cannot be copied. As an example, we’ll write a function to compare the length of two strings. Because strings can be long, we’d like to avoid copying them, so we’ll make our parameters references.

Arrays have two special properties that affect how we define and use functions that operate on arrays: We cannot copy an array, and when we use an array it is (usually) converted to a pointer. Because we cannot copy an array, we cannot pass an array by value. Because arrays are converted to pointers, when we pass an array to a function, we are actually passing a pointer to the array’s first element.

Even though we cannot pass an array by value, we can write a parameter that looks array: Regardless of appearances, these declarations are equivalent: Each declares a function with a single parameter of type const int*. When the compiler checks a call to print, it checks only that the argument has type const int*:

void print(const int*);
void print(const int[]); // shows the intent that the function takes an array
void print(const int[10]); // dimension for documentation purposes (at best)

Because arrays are passed as pointers, functions ordinarily don’t know the size of the array they are given. They must rely on additional information provided by the caller. There are three common techniques used to manage pointer parameters.

A technique used to manage array arguments is to pass pointers to the first and one past the last element in the array. This approach is inspired by techniques used in the standard library. Using this approach, we’ll print the elements in an array as follows:

#include <iostream>
using std::cin; using std::cout; using std::endl;

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

int main(){
    int arr[4] = { 2,4,2,4 };
    print(std::begin(arr), std::end(arr));
    return 0;
}

Another approach for array arguments, which is common in C programs and older C++ programs, is to define a second parameter that indicates the size of the array. Using this approach, we’ll rewrite print as follows:

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

In c++ multidimensional array is an array of arrays. As with any array, a multidimensional array is passed as a pointer to its first element. Because we are dealing with an array of arrays, that element is an array, so the pointer is a pointer to an array. The size of the second (and any subsequent) dimension is part of the element type and must be specified:

void print(int (*arr)[2], size_t size) {
    for (size_t i = 0; i < size; i++)
    {
        auto beg = std::begin(*arr), end = std::end(*arr);
        while (beg != end)
        {
            cout << *beg++ << endl;
        }
        arr += 1;
    }
}
//We can also define our function using array syntax.
void print(int arr[][2], size_t size) { /* . . . */ }

Up to now, we have defined main with an empty parameter list: int main() { ... }, However, we sometimes need to pass arguments to main. The most common use of arguments to main is to let the user specify a set of options to guide the operation of the program. For example, assuming our main program is in an executable file named prog, we might pass options to the program as follows:prog -d -o ofile data0.

Such command-line options are passed to main in two (optional) parameters: int main(int argc, char *argv[]) { ... }, The second parameter, argv, is an array of pointers to C-style character strings. The
first parameter, argc, passes the number of strings in that array.

Sometimes we do not know in advance how many arguments we need to pass to a function. For example, we might want to write a routine to print error messages generated from our program. We’d like to use a single function to print these error messages in order to handle them in a uniform way. However, different calls to our error-printing function might pass different arguments, corresponding to different kinds of error messages.

The new standard provides two primary ways to write a function that takes a varying number of arguments: If all the arguments have the same type, we can pass a library type named initializer_list. An initializer_list is a library type that represents an array of values of the specified type. This type is defined in the initializer_list header. You can see all details in here.

void error_msg(std::initializer_list<std::string> il){
    for (auto beg = il.begin(); beg != il.end(); ++beg)
        cout << *beg << " ";
    cout << endl;
}

When we pass a sequence of values to an initializer_list parameter, we must enclose the sequence in curly braces:

error_msg({ "4tw", "98ty8w" });

Return Types and the return Statement

A return statement terminates the function that is currently executing and returns control to the point from which the function was called. There are two forms of return statements: return; and return expression;.

A return with no value may be used only in a function that has a return type of void. Functions that return void are not required to contain a return. In a void function, an implicit return takes place after the function’s last statement. The second form of the return statement provides the function’s result. Every return in a function with a return type other than void must return a value.

Note never return a reference or pointer to a local object, When a function completes, its storage is freed. After a function terminates, references to local objects refer to memory that is no longer valid:

//don't do this
std::string& em() {
    std::string str = "iecbvasj";
    return str;
}

A function that calls itself, either directly or indirectly, is a recursive function. As an example, we can rewrite our factorial function to use recursion:

int factorial(int val){
    if (val > 1)
        return factorial(val-1) * val;
    return 1;
}

Because we cannot copy an array, a function cannot return an array. However, a function can return a pointer or a reference to an array. There are some ways to simplify such declarations, the most straightforward way is to use a type alias:

typedef int arrT[10];  // arrT is a synonym for the type array of ten ints
using arrT = int[10]; // equivalent declaration of arrT;

arrT is a synonym for an array of ten ints. Because we cannot return an array, we define the return type as a pointer to this type.

// a function that initiated a array
arrT *init_arr(arrT *t){
    for (auto i = std::begin(*t); i != std::end(*t); i++){
        *i = 1;
    }
    return t;
}

To declare function without using a type alias, we must remember that the dimension of an array follows the name being defined:

int (*init_arr1(int (*t)[10]))[10] {
    for (auto i = std::begin(*t); i != std::end(*t); i++) {
        *i = 1;
    }
    return t;
}

Under the new standard, another way to simplify the declaration of func is by using a trailing return type. Trailing returns can be defined for any function, but are most useful for functions with complicated return types, such as pointers (or references) to arrays.

auto init_arr(arrT* t) -> int(*)[10]{
    for (auto i = std::begin(*t); i != std::end(*t); i++) {
        *i = 1;
    }
    return t;
}

Overloaded Functions

Functions that have the same name but different parameter lists and that appear in the same scope are overloaded. For example, we want write a print function that print array with different parameter:

void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

When we call these functions, the compiler can deduce which function we want based on the argument type we pass, Overloaded functions must differ in the number or the type(s) of their parameters.

As we saw in previous top-level const has no effect on the objects that can be passed to the function. A parameter that has a top-level const is indistinguishable from one without a top-level const:

int f1(const int a);
int f1(int a);      //same
int f1(int *const a);
int f1(int* a);     //same

In these declarations, the second declaration declares the same function as the first.But we can overload based on whether the parameter is a reference (or pointer) to the const or nonconst version of a given type; such consts are low-level: because a low level pointer or reference refer a const object, In these cases, the compiler can use the constness of the argument to distinguish which function to call:

#include <iostream>
using std::cin; using std::cout; using std::endl;

int f1(const int* a) {
    return 1;
}

int f1(int* a) {
    return 2;
}

int f1(const int& a) {
    return 3;
}

int f1(int& a) {
    return 4;
}

int main() {
    const int a = 0;
    int b = 0;
    cout << f1(&a) << endl;  //print 1
    cout << f1(&b) << endl;  //print 2
    cout << f1(a) << endl;   //print 3
    cout << f1(b) << endl;   //print 4
    return 0;
}

Note overloading has no special properties with respect to scope: As usual, if we declare a name in an inner scope, that name hides uses of that name declared in an outer scope. Names do not overload across scopes:

#include <iostream>
using std::cin; using std::cout; using std::endl;

int z() {
    return 10;
}

int p() {
    return 1;
}

int main() {
    bool z;
    int r = z();    //error: z is a bool object
    double p(int a);
    p();            //error: previous function is hided
    p(1);           //ok
    return 0;
}

Features for Specialized Uses

In this section we’ll cover three function-related features that are useful in many, but not all, programs: default arguments, inline and constexpr functions.

Some functions have parameters that are given a particular value in most, but not all. In such cases, we can declare that common value as a default argument for the function. Functions with default arguments can be called with or without thatargument.

For example, we want say hello to different program languages, in default case we say hello to c++ once:

#include <iostream>
#include <string>
using std::cin; using std::cout; using std::endl;

void hello(std::string s = "C++", size_t n = 1) {
    for (size_t i = 0; i < n; i++)
    {
        cout << "Hello " << s << endl;
    }
}

int main() {
    hello();       // say hello C++ once
    hello("Java"); // say hello Java once
    hello("C#", 2);// say hello C# twice
    hello(3);      // error: can omit only trailing arguments
    return 0;
}

Sometimes we defining a function with such a small operation, the benefit of function are:

  1. It is easier to read and understand a call to function than it would be to read and understand the equivalent conditional expression.
  2. Using a function ensures uniform behavior. Each test is guaranteed to be done the same way.
  3. If we need to change the computation, it is easier to change the function than to find and change every occurrence of the equivalent expression.
  4. The function can be reused rather than rewritten for other applications.

However, Calling a function is apt to be slower than evaluating the equivalent expression. On most machines, a function call does a lot of work: Registers are saved before the call and restored after the return; arguments may be copied; and the program branches to a new location. Fortunately we have a good way to fix it disadvantage. The Inline Functions

A function specified as inline (usually) is expanded “in line” at each call. If function were defined as inline, then this call (probably) would be expanded during compilation into expression.

inline const string &shorterString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}
cout << shorterString(s1, s2) << endl;
//may be compilation into expression. 
//cout << (s1.size() < s2.size() ? s1 : s2) << endl;

In general, the inline mechanism is meant to optimize small, straight-line functions that are called frequently.

Function Matching

In many (if not most) cases, it is easy to figure out which overloaded function matches a given call. However, it is not so simple when the overloaded functions have the same number of parameters and when one or more of the parameters have types that are related by conversions.

The step of function matching as follow:

  1. The first step of function matching identifies the set of overloaded functions considered for the call. The functions in this set are the candidate functions. A candidate function is a function with the same name as the called function and for which a declaration is visible at the point of the call.
  2. The second step selects from the set of candidate functions those functions that can be called with the arguments in the given call. The selected functions are the viable functions. To be viable, a function must have the same number of parameters as there are arguments in the call, and the type of each argument must match—or be convertible to—the type of its corresponding parameter.
  3. The third step of function matching determines which viable function provides the best match for the call. This process looks at each argument in the call and selects the viable function (or functions) for which the corresponding parameter best matches the argument. The idea is that the closer the types of the argument and parameter are to each other, the better the match.

In order to determine the best match, the compiler ranks the conversions that could be used to convert each argument to the type of its corresponding parameter. Conversions are ranked as follows:

  1. An exact match.
  2. Match through a const conversion
  3. Match through a promotion
  4. Match through an arithmetic or pointer conversion
  5. Match through a class-type conversion.

Pointers to Functions

A function pointer is just that—a pointer that denotes a function rather than an object. Like any other pointer, a function pointer points to a particular type. A function’s type is determined by its return type and the types of its parameters. The function’s name is not part of its type. For example:

// compare two int
bool cmp(int a, int b) {
    return a > b;
}
// pf points to a function returning bool that takes two int
bool (*pf)(int a, int b);

When we use the name of a function as a value, the function is automatically converted to a pointer. For example, we can assign the address of cmp to pf as follows:

pf = cmp;
pf = &cmp;

Moreover, we can use a pointer to a function to call the function to which the pointer points. We can do so directly—there is no need to dereference the pointer:

bool a = (*cmp)(1, 2);
bool b = cmp(1, 2);
bool c = (*pf)(1, 2);
bool d = pf(1, 2);

As usual, when we use an overloaded function, the context must make it clear which version is being used. When we declare a pointer to an overloaded function the compiler uses the type of the pointer to determine which overloaded function to use. The type of the pointer must match one of the overloaded functions exactly:

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // ok: pf1 points to ff(unsigned int)
void (*pf2)(int) = ff;          // error: no ff with a matching parameter list
double (*pf3)(int*) = ff;       // error: return type of ff and pf3 don't match

Just as with arrays, we cannot define parameters of function type but can have a parameter that is a pointer to function. As with arrays, we can write a parameter that looks like a function type, but it will be treated as a pointer:

void useBigger(const string& s1, const string& s2,
               bool pf(const string&, const string&));
// equivalent declaration: explicitly define the parameter as a pointer to function
void useBigger(const string& s1, const string& s2,
               bool (*pf)(const string&, const string&));

When we pass a function as an argument, we can do so directly. It will be automatically converted to a pointer.

As with arrays, we can’t return a function type but can return a pointer to a function type. Similarly, we must write the return type as a pointer type; the compiler will not automatically treat a function return type as the corresponding pointer type. Also as with array returns, by far the easiest way to declare a function that returns a pointer to function is by 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
pF f1(int); // ok: pF is a pointer to function; f1 returns a pointer to function
F f2(int);  // error: F is a function type; f1 can't return a function
F *f3(int); // ok: explicitly specify that the return type is a pointer to function

For completeness, it’s worth noting that we can simplify declarations of functions that return pointers to function by using a trailing return:

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

Classes

In C++ we use classes to define our own data types. By defining types that mirror concepts in the problems we are trying to solve, we can make our programs easier to write, debug, and modify.

Defining Abstract Data Types

Our Sales_data class(in previous note) is not an abstract data type. It lets users of the class access its data members and forces users to write their own operations. To make Sales_data an abstract type, we need to define operations for users of Sales_data to use. Once Sales_data defines its own operations, we can encapsulate (that is, hide) its data members.

Ultimately, we want Sales_data to support the same set of operations as the Sales_item class. The Sales_item class had one member function named isbn, and supported the +, =, +=, <<, and >> operators. We’ll learn how to define our own operators in the next post. For now, we’ll define ordinary (named) functions for these operations.

Thus, the interface to Sales_data consists of the following operations:

  • An isbn member function to return the object’s ISBN
  • A combine member function to add one Sales_data object into another
  • A function named add to add two Sales_data objects
  • A read function to read data from an istream into a Sales_data object
  • A print function to print the value of a Sales_data object on an ostream

Before we think about how to implement our class, let’s look at how we can use our interface functions. As one example, we can use these functions to write a version of the bookstore program that works with Sales_data objects rather than Sales_items:

#include <iostream>
#include <string>
#include "Sales_data.h"
using std::cin; using std::cout; using std::endl;

int main() {
    Sales_data total; // variable to hold the running sum
    if (read(cin, total)) { // read the first transaction
        Sales_data trans; // variable to hold data for the next transaction
        while (read(cin, trans)) { // read the remaining transactions
            if (total.isbn() == trans.isbn()) // check the isbns
                total.combine(trans); // update the running total
            else {
                print(cout, total) << endl; // print the results
                total = trans; // process the next book
            }
        }
        print(cout, total) << endl; // print the last transaction
    }
    else { // there was no input
        std::cerr << "No data?!" << endl; // notify the user
    }
    return 0;
}

Our revised class will have the same data members as the version we defined in previous: bookNo, a string representing the ISBN; units_sold, an unsigned that says how many copies of the book were sold; and revenue, a double representing the total revenue for those sales.

As we’ve seen, our class will also have two member functions, combine and isbn. In addition, we’ll give Sales_data another member function to return the average price at which the books were sold. This function, which we’ll name avg_price, isn’t intended for general use. It will be part of the implementation, not part of the interface.

We define and declare member functions similarly to ordinary functions. Member functions must be declared inside the class. Member functions may be defined inside the class itself or outside the class body. Nonmember functions that are part of the interface, such as add, read, and print, are declared and defined outside the class.

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
    // new members: operations on Sales_data objects
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
// nonmember Sales_data interface functions
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
#endif

We’ll start by explaining the isbn function, which returns a string and has an empty parameter list. As with any function, the body of a member function is a block. In this case, the block contains a single return statement that returns the bookNo data member of a Sales_data object. The interesting thing about this function is how it gets the object from which to fetch the bookNo member.

Let’s look again at a call to the isbn member function: total.isbn(),Here we use the dot operator to fetch the isbn member of the object named total, which we then call. When isbn refers to members of
Sales_data, it is referring implicitly to the members of the object on which the function was called. In this call, when isbn returns bookNo, it is implicitly returning total.bookNo.

Member functions access the object on which they were called through an extra, implicit parameter named this. When we call a member function, this is initialized with the address of the object on which the function was invoked. For example, when we call total.isbn(), the compiler passes the address of total to the implicit this parameter in isbn. It is as if the compiler rewrites this call as Sales_data::isbn(&total), which calls the isbn member of Sales_data passing the address of total.

The this parameter is defined for us implicitly. Indeed, it is illegal for us to define a parameter or variable named this. Inside the body of a member function, we can use this. It would be legal, although unnecessary, to define isbn as:

std::string isbn() const { return this->bookNo; }

The other important part about the isbn function is the keyword const that follows the parameter list. The purpose of that const is to modify the type of the implicit this pointer.

By default, the type of this is a const pointer to the nonconst version of the class type. For example, by default, the type of this in a Sales_data member function is Sales_data *const. Although this is implicit, it follows the normal initialization rules, which means that (by default) we cannot bind this to a const object. This fact, in turn, means that we cannot call an ordinary member function on a const object.

If isbn were an ordinary function and if this were an ordinary pointer parameter, we would declare this as const Sales_data *const. After all, the body of isbn doesn’t change the object to which this points, so our function would be more flexible if this were a pointer to const. Member functions that use const in this way are const member functions.

The fact that this is a pointer to const means that const member functions cannot change the object on which they are called. Thus, isbn may read but not write to the data members of the objects on which it is called.

The definitions of the member functions of a class are nested inside the scope of the class itself. Hence, isbn’s use of the name bookNo is resolved as the data member defined inside Sales_data. It is worth noting that isbn can use bookNo even though bookNo is defined after isbn. Because the compiler processes classes in two steps— the member declarations are compiled first, after which the member function bodies, if any, are processed. Thus, member function bodies may use other members of their class regardless of where in the class those members appear.

As with any other function, when we define a member function outside the class body, the member’s definition must match its declaration. That is, the return type, parameter list, and name must match the declaration in the class body.

// define avg price function
double Sales_data::avg_price() const {
    if (units_sold)
        return revenue / units_sold;
    else
        return 0;
}

The function name, Sales_data::avg_price, uses the scope operator to say that we are defining the function named avg_price that is declared in the scope of the Sales_data class. Once the compiler sees the function name, the rest of the code is interpreted as being inside the scope of the class. Thus, when avg_price refers to revenue and units_sold, it is implicitly referring to the members of Sales_data.

The combine function is intended to act like the compound assignment operator, +=. The object on which this function is called represents the left-hand operand of the assignment. The right-hand operand is passed as an explicit argument:

// define combine function
Sales_data& Sales_data::combine(const Sales_data& rhs) {
    units_sold += rhs.units_sold; // add the members of rhs into
    revenue += rhs.revenue; // the members of ''this'' object
    return *this; // return the object on which the function was called
}

When our transaction-processing program calls total.combine(trans);, the address of total is bound to the implicit this parameter and rhs is bound to trans. At the end, Here the return statement dereferences this to obtain the object on which the function is executing. That is, for the call above, we return a reference to total.

Class authors often define auxiliary functions, such as our add, read, and print functions. Although such functions define operations that are conceptually part of the interface of the class, they are not part of the class itself.

We define nonmember functions as we would any other function. As with any other function, we normally separate the declaration of the function from its definition. Functions that are conceptually part of a class, but not defined inside the class, are typically declared (but not defined) in the same header as the class itself. That way users need to include only one file to use any part of the interface.

The read and print functions do the same job as the code in previous code:

std::ostream& print(std::ostream& os, const Sales_data& item) {
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    return os;
}
std::istream& read(std::istream& is, Sales_data& item) {
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = item.units_sold * price;
    return is;
}

The add function takes two Sales_data objects and returns a new Sales_data representing their sum:

Sales_data add(const Sales_data& lhs, const Sales_data& rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

Each class defines how objects of its type can be initialized. Classes control object initialization by defining one or more special member functions known as constructors. The job of a constructor is to initialize the data members of a class object. A constructor is run whenever an object of a class type is created.

Constructors have the same name as the class. Unlike other functions, constructors have no return type. Like other functions, constructors have a (possibly empty) parameter list and a (possibly empty) function body. A class can have multiple constructors. Like any other overloaded function, the constructors must differ from each other in the number or types of their parameters.

Our Sales_data class does not define any constructors, yet the programs we’ve written that use Sales_data objects compile and run correctly. So, How are they initialized? The answer is if our class does not explicitly define any constructors, the compiler will implicitly define the default constructor for us.

For our Sales_data class we’ll define four constructors with the following parameters:

  1. An istream& from which to read a transaction.
  2. A const string& representing an ISBN, an unsigned representing the count of how many books were sold, and a double representing the price at which the books sold.
  3. A const string& representing an ISBN. This constructor will use default values for the other members.
  4. An empty parameter list (i.e., the default constructor) which as we’ve just seen we must define because we have defined other constructors.
// constructors added
// means default constructor
Sales_data() = default;
Sales_data(std::string& s) : bookNo(s) {}
Sales_data(std::string& s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p* n) {}
Sales_data(std::istream&);

It is worth noting that both constructors have empty function bodies. The only work these constructors need to do is give the data members their values. If there is no further work, then the function body is empty.

Unlike our other constructors, the constructor that takes an istream does have work to do. Inside its function body, this constructor calls read to give the data members new values:

Sales_data::Sales_data(std::istream& is) {
    read(is, *this);
}

There are something need to notice:

  1. As with any other member function, when we define a constructor outside of the class body, we must specify the class of which the constructor is a member.
  2. In this constructor there is no constructor initializer list, although technically speaking, it would be more correct to say that the constructor initializer list is empty. Even though the constructor initializer list is empty, the members of this object are still initialized before the constructor body is executed.

Access Control and Encapsulation

At this point, we have defined an interface for our class; but nothing forces users to use that interface. Our class is not yet encapsulated—users can reach inside a Sales_data object and meddle with its implementation. In C++ we use access specifiers to enforce encapsulation:

  1. Members defined after a public specifier are accessible to all parts of the program. The public members define the interface to the class.
  2. Members defined after a private specifier are accessible to the member functions of the class but are not accessible to code that uses the class.
struct Sales_data {
    public:
    // constructors added
    Sales_data() = default;
    Sales_data(std::string& s) : bookNo(s) {}
    Sales_data(std::string& s, unsigned n, double p) :
    bookNo(s), units_sold(n), revenue(p* n) {}
    Sales_data(std::istream&);
    // new members: operations on Sales_data objects
    std::string isbn() const;
    Sales_data& combine(const Sales_data&);
    private:
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

A class may contain zero or more access specifiers, and there are no restrictions on how often an access specifier may appear. Each access specifier specifies the access level of the succeeding members. The specified access level remains in effect until the next access specifier or the end of the class body.

We also made another, more subtle, change: We used the class keyword rather than struct to open the class definition. This change is strictly stylistic; we can define a class type using either keyword. The only difference between struct and class is the default access level.

A class may define members before the first access specifier. Access to such members depends on how the class is defined. If we use the struct keyword, the members defined before the first access specifier are public; if we use class, then the members are private.

Now that the data members of Sales_data are private, our read, print, and add functions will no longer compile. The problem is that although these functions are part of the Sales_data interface, they are not members of the class.

A class can allow another class or function to access its nonpublic members by making that class or function a friend. A class makes a function its friend by including a declaration for that function preceded by the keyword friend:

// add into class
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream& print(std::ostream&, const Sales_data&);
friend std::istream& read(std::istream&, Sales_data&);

Encapsulation provides two important advantages:

  • User code cannot inadvertently corrupt the state of an encapsulated object.
  • The implementation of an encapsulated class can change over time without requiring changes in user-level code.

Additional Class Features

The Sales_data class is pretty simple, in the next we’ll cover some additional class-related features that Sales_data doesn’t need to use. These features include type members, in-class initializers for members of class type, mutable data members, inline member functions, returning *this from a member function, more about how we define and use class types, and class friendship.

First we will talk about type members, suppose we want use a class called Screen represents a window on a display. Each Screen has a string member that holds the Screen’s contents, and three string::size_type members that represent the position of the cursor, and the height and width of the screen.

In addition to defining data and function members, a class can define its own local names for types. Type names defined by a class are subject to the same access controls as any other member and may be either public or private:

class Screen {
public:
    using pos = std::string::size_type;
private:
    std::string contents;
    pos height;
    pos height;
    pos cursor;
};

We defined pos in the public part of Screen because we want users to use that name. we can use Screen::pos k = 10; after include header file.

#ifndef Screen_H
#define Screen_H
#include <string>
class Screen {
public:
    using pos = std::string::size_type;
    // default constructor and another constructor
    Screen() = default;
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht* wd, c) {}

    // get the character at the cursor
    // implicitly inline
    char get() const {
        return contents[cursor];
    }
    // explicitly inline
    inline char get(pos r, pos c) const {
        pos row = r * width;
        return contents[row + c];
    }
    // move cursor position
    Screen& move(pos r, pos c) {
        pos row = r * width;
        cursor = row + c;
        return *this;
    }
    // test
    void test_mutable() const {
        ++access_ctr;
    }
private:
    mutable pos access_ctr = 0;
    std::string contents;
    pos height = 0;
    pos width = 0;
    pos cursor = 0;
};
#endif // !Screen_H

Classes often have small functions that can benefit from being inlined. Member functions defined inside the class are automatically inline.

As with nonmember functions, member functions may be overloaded so long as the functions differ by the number and/or types of parameters. In the screen class, the second get function is a overload function.

Screen::pos h = 10;
Screen::pos w = 10;
Screen myscreen(h,w,'A');
cout << myscreen.get(0,0) << endl;
cout << myscreen.get() << endl;

It sometimes (but not very often) happens that a class has a data member that we want to be able to modify, even inside a const member function. We indicate such members by including the mutable keyword in their declaration.

A mutable data member is never const, even when it is a member of a const object. Accordingly, a const member function may change a mutable member. As an example, we’ll give Screen a mutable member named access_ctr, and a const member function that change the value of mutable member. The function can be correct use.

Next we’ll add functions to set the character at the cursor or at a given location:

#ifndef Screen_H
#define Screen_H
#include <string>
class Screen
{
public:
    // other members function as before
    // set char in screen
    Screen &set(char c)
    {
        contents[cursor] = c;
        return *this;
    }
    Screen &set(pos r, pos col, char c)
    {
        contents[r * width + col] = c;
        return *this;
    }
    // other members as before
};
#endif // !Screen_H

Like the move operation, our set members return a reference to the object on which they are called. Functions that return a reference are lvalues, which means that they return the object itself, not a copy of the object. If we concatenate a sequence of these actions into a single expression:

myscreen.move(4, 0).set('B');

That is, this statement is equivalent to:

myscreen.move(4, 0);
myscreen.set('B');

Next, we’ll add an operation, which we’ll name display, to print the contents of the Screen. We’d like to be able to include this operation in a sequence of set and move operations. Therefore, like set and move, our display function will return a reference to the object on which it executes.

Logically, displaying a Screen doesn’t change the object, so we should make display a const member. If display is a const member, then this is a pointer to const and *this is a const object. Hence, the return type of display must be const Sales_data&. However, if display returns a reference to const, we won’t be able to embed display into a series of actions:

myScreen.display(std::cout).set('B');

We can overload a member function based on whether it is const for the same reasons that we can overload a function based on whether a pointer parameter points to const. The nonconst version will not be viable for const objects; we can only call const member functions on a const object. We can call either version on a nonconst object, but the nonconst version will be a better match.

public:
//print screen
const Screen& print(std::ostream& os)const {
    do_display(os);
    return *this;
}
Screen& print(std::ostream& os) {
    do_display(os);
    return *this;
}
private:
void do_display(std::ostream& os) const {
    for (size_t i = 0; i < height; i++) {
        for (size_t j = 0; j < width; j++) {
            os << get(i, j);
        }
        os << std::endl;
    }
}

Our Sales_data class defined three ordinary nonmember functions as friends. A class can also make another class its friend or it can declare specific member functions of another (previously defined) class as friends. In addition, a friend function can be defined inside the class body. Such functions are implicitly inline.

For example, we want use a class called Windows_mgr to control a set of screen, such as clear screen. To do this job, our function called clear needs to access the private data members of Screen. To allow this access, Screen can designate Window_mgr as its friend:

class Screen {
    // Window_mgr members can access the private parts of class Screen
    friend class Windows_mgr;
    // ... rest of the Screen class
};
class Windows_mgr {
    public:
    using ScreenIndex = std::vector<Screen>::size_type;
    Windows_mgr() = default;

    // clear screen
    void clear(ScreenIndex i) {
        Screen& s = screens[i];
        s.contents = std::string(s.width * s.height, ' ');
    }

    private:
    std::vector<Screen> screens{ Screen(10,10,' ') };
};

Rather than making the entire Window_mgr class a friend, Screen can instead specify that only the clear member is allowed access. When we declare a member function to be a friend, we must specify the class of which that function is a member:

class Screen {
    // clear function in Windoes_mgr can access the private parts of class Screen
    friend void Windows_mgr::clear(ScreenIndex i);
    // ... rest of the Screen class
};

Making a member function a friend requires careful structuring of our programs to accommodate interdependencies among the declarations and definitions. In this example, we must order our program as follows:

  1. First, define the Window_mgr class, which declares, but cannot define, clear. Screen must be declared before clear can use the members of Screen.
  2. Next, define class Screen, including a friend declaration for clear.
  3. Finally, define clear, which can now refer to the members in Screen.

The full code:

#ifndef Screen_H
#define Screen_H
#include <string>
#include <vector>

class Screen;

class Windows_mgr {
    public:
    using ScreenIndex = std::vector<Screen>::size_type;
    Windows_mgr() = default;

    // clear screen
    void clear(ScreenIndex i);
    // add a new screen
    void add();

    private:
    std::vector<Screen> screens;
};

class Screen {
    public:
    friend void Windows_mgr::clear(ScreenIndex i);
    using pos = std::string::size_type;
    // default constructor and another constructor
    Screen() = default;
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht* wd, c) {}

    // get the character at the cursor
    // implicitly inline
    char get() const {
        return contents[cursor];
    }
    // explicitly inline
    inline char get(pos r, pos c) const {
        pos row = r * width;
        return contents[row + c];
    }

    // move cursor position
    Screen& move(pos r, pos c) {
        pos row = r * width;
        cursor = row + c;
        return *this;
    }

    // test mutable mumber
    void test_mutable() const {
        ++access_ctr;
    }

    //set char in screen
    Screen& set(char c) {
        contents[cursor] = c;
        return *this;
    }
    Screen& set(pos r, pos col, char c) {
        contents[r * width + col] = c;
        return *this;
    }

    //print screen
    const Screen& print(std::ostream& os)const {
        do_display(os);
        return *this;
    }
    Screen& print(std::ostream& os) {
        do_display(os);
        return *this;
    }

    private:
    mutable pos access_ctr = 0;
    std::string contents;
    pos height = 0;
    pos width = 0;
    pos cursor = 0;

    void do_display(std::ostream& os) const {
        for (size_t i = 0; i < height; i++) {
            for (size_t j = 0; j < width; j++) {
                os << get(i, j);
            }
            os << '\n';
        }
    }
};

void Windows_mgr::clear(ScreenIndex i) {
    Screen& s = screens[i];
    s.contents = std::string(s.width * s.height, ' ');
}

void Windows_mgr::add() {
    Screen new_s(10, 10, ' ');
    screens.push_back(new_s);
}

#endif // !Screen_H

Class Scope

In the programs we’ve written so far, name lookup (the process of finding which declarations match the use of a name) has been relatively straightforward:

  • First, look for a declaration of the name in the block in which the name was used. Only names declared before the use are considered.
  • If the name isn’t found, look in the enclosing scope(s).
  • If no declaration is found, then the program is in error.

The way names are resolved inside member functions defined inside the class may seem to behave differently than these lookup rules. However, in this case, appearances are deceiving. Class definitions are processed in two phases:

  • First, the member declarations are compiled.
  • Function bodies are compiled only after the entire class has been seen.

Because member function bodies are not processed until the entire class is seen, they can use any name defined inside the class. If function definitions were processed at the same time as the member declarations, then we would have to order the member functions so that they referred only to names already seen.

Constructors Revisited

Constructors are a crucial part of any C++ class. In this section we’ll cover some additional capabilities of constructors, and deepen our coverage of the material introduced earlier.

When we define variables, we typically initialize them immediately rather than defining them and then assigning to them, exactly the same distinction between initialization and assignment applies to the data
members of objects. If we do not explicitly initialize a member in the constructor initializer list, that member is default initialized before the constructor body starts executing. For example:

// legal but sloppier way to write the Sales_data constructor: no constructor initializers
Sales_data::Sales_data(const string& s,
                       unsigned cnt, double price) {
    bookNo = s;
    units_sold = cnt;
    revenue = cnt * price;
}

This version and our original definition in previous have the same effect: When the constructor finishes, the data members will hold the same values. The difference is that the original version initializes its data members, whereas this version assigns values to the data members. How significant this distinction is depends on the type of the data member.

We can often, but not always, ignore the distinction between whether a member is initialized or assigned. Members that are const or references must be initialized.

#pragma once
class test {
    public:
    //ok:
    test(int n) : a(n) {}
    //error:
    test(int n){
        a = n;
    }

    private:
    const int a;
};

By the time the body of the constructor begins executing, initialization is complete. Our only chance to initialize const or reference data members is in the constructor initializer.

Another need notice in c++ is Order of Member Initialization, Members are initialized in the order in which they appear in the class definition: The first member is initialized first, then the next, and so on. The order in which initializers appear in the constructor initializer list does not change the order of initialization. The order of initialization often doesn’t matter. However, if one member is initialized in terms of another, then the order in which members are initialized is crucially important.

We can set default arguments in constructors:

test(int n = 10) : a(n) {}

The new standard extends the use of constructor initializers to let us define so-called delegating constructors. A delegating constructor uses another constructor from its own class to perform its initialization. It is said to “delegate” some (or all) of its work to this other constructor.

As an example, we’ll rewrite the Sales_data class to use delegating constructors as follows:

class Sales_data {
    public:
    // nondelegating constructor initializes members from corresponding arguments
    Sales_data(std::string s, unsigned cnt, double price) :
    bookNo(s), units_sold(cnt), revenue(cnt* price) {
    }
    // remaining constructors all delegate to another constructor
    Sales_data() : Sales_data("", 0, 0) {}
    Sales_data(std::string s) : Sales_data(s, 0, 0) {}
    Sales_data(std::istream& is) : Sales_data() {
        read(is, *this);
    }
    // other members as before
};

static Class Members

Classes sometimes need members that are associated with the class, rather than with individual objects of the class type. We say a member is associated with the class by adding the keyword static to its declaration. Like any other member, static members can be public or private. The type of a static data member can be const, reference, array, class type, and so forth.

As an example, we’ll define a class to represent an account record at a bank:

#ifndef Account_H
#define Account_H
#include <string>
class Account {
    public:
    Account() = default;

    static double get_rate() {
        return rate;
    }

    static void set_rate(double r) {
        rate = r;
    }

    private:
    std::string user;
    double amount;
    static double rate;
};
//define static member
double Account::rate = 0.001;
#endif

The static members of a class exist outside any object. Objects do not contain data associated with static data members. Thus, each Account object will contain two data members—user and amount. There is only one rate object that will be shared by all the Account objects.

How could we use a class static member? First, we can directly through the scope operator:

double r;
r = Account::get_rate(); // access a static member using the scope operator

Even though static members are not part of the objects of its class, we can use an object, reference, or pointer of the class type to access a static member:

Account s;
double r;
r = s.get_rate();

Member functions can use static members directly, without the scope operator.

As with any other member function, we can define a static member function inside or outside of the class body. When we define a static member outside the class, we do not repeat the static keyword. The keyword appears only with the declaration inside the class body.

Because static data members are not part of individual objects of the class type, they are not defined when we create objects of the class. As a result, they are not initialized by the class’ constructors. Moreover, in general, we may not initialize a static member inside the class. Instead, we must define and initialize each static data member outside the class body. Like any other object, a static data member may be defined only once.


life is perfact