2-Variables and Basic Types

本文详细介绍了C++中的基本内置类型,包括算术类型(如整型、浮点型、字符型)及其转换,以及变量的定义、声明、作用域等概念。此外,还探讨了复合类型如指针、引用,并讨论了const限定符在不同情况下的使用,最后讲解了如何定义自己的数据结构。文章包含多个练习题,帮助读者巩固理解。

Please indicate the source: http://blog.youkuaiyun.com/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

  • Types determine the meaning of the data and operations.

2.1. Primitive Built-in Types

  • C++ defines a set of primitive types that include the arithmetic types and a special type named void.
    1. The arithmetic types represent characters, integers, boolean values, and floating-point numbers.
    2. The void type has no associated values and can be used in only a few circumstances, most commonly as the return type for functions that do not return a value.

2.1.1. Arithmetic Types

  • The arithmetic types are divided into two categories: integral types (which include character and boolean types) and floating-point types.

#include <iostream>

using std::cout;

int main()
{
    cout << "Character:\t" << sizeof(char) << '\t' << sizeof(wchar_t) << '\t' <<
         sizeof(char16_t) << '\t' << sizeof(char32_t) << '\n';

    cout << "Boolean:\t" << sizeof(bool) << '\n';

    cout << "Integer:\t" << sizeof(short) << '\t' << sizeof(int) << '\t' <<
         sizeof(long) << '\t' << sizeof(long long) << '\n';

    cout << "Floating-point:\t" << sizeof(float) << '\t' << sizeof(double) << '\t' <<
         sizeof(long double) << '\n';

    return 0;
}
/*
Output:
Character:      1   4   2   4
Boolean:        1
Integer:            2   4   8   8
Floating-point: 4   8   16
*/
  • The size of (i.e., the number of bits in) the arithmetic types varies across machines. The standard guarantees minimum sizes as listed in Table 2.1. Compilers are allowed to use larger sizes for these types. Because the number of bits varies, the largest (or smallest) value that a type can represent also varies.

  • The bool type represents the truth values true and false.
  • The basic character type is char. A char is the same size as a single machine byte, which is big enough to hold numeric values corresponding to the characters in the machine’s basic character set.
  • The remaining character types—wchar_t, char16_t, and char32_t—are used for extended character sets. The wchar_t type is guaranteed to be large enough to hold any character in the machine’s largest extended character set. The types char16_t and char32_t are intended for Unicode characters. (Unicode is a standard for representing characters used in any natural language.)
  • The remaining integral types represent integer values of different sizes. The language guarantees that an int will be at least as large as short, a long at least as large as an int, and long long at least as large as long. The type long long was introduced by the new standard(C++11).
  • The floating-point types represent single-, double-, and extended-precision values. The standard specifies a minimum number of significant digits. Typically, floats are represented in one word (32 bits), doubles in two words (64 bits), and long doubles in either three or four words (96 or 128 bits). The float and double types typically yield about 7 and 16 significant digits, respectively. The type long double is used as a way to accommodate special-purpose floating-point hardware; its precision varies from one implementation to another.

Machine-Level Representation of the Built-in Types

  • Computers store data as a sequence of bits, each holding a 0 or 1. Most computers deal with memory as chunks of bits of sizes that are powers of 2. The smallest chunk of addressable memory is referred to as a “byte.” The basic unit of storage, usually a small number of bytes, is referred to as a “word.”
  • In C++ a byte has at least as many bits as are needed to hold a character in the machine’s basic character set. On most machines a byte contains 8 bits and a word is either 32 or 64 bits, that is, 4 or 8 bytes.
  • Most computers associate an address with each byte in memory. On a machine with 8-bit bytes and 32-bit words, we might view a word of memory as follows: the byte’s address is on the left, with the 8 bits of the byte following the address.

  • We can use an address to refer to any of variously sized collections of bits starting at that address. The type determines how many bits are used and how to interpret those bits.

Signed and Unsigned Types

  • A signed type represents negative or positive numbers (including zero); an unsigned type represents only values greater than or equal to zero.
  • The types int, short, long, and long long are all signed. We obtain the corresponding unsigned type by adding unsigned to the type(unsigned short…). The type “unsigned int” can be abbreviated as unsigned.
  • There are three distinct basic character types: char, signed char, and unsigned char. char is not the same type as signed char. But there are only two representations: signed and unsigned. The char type uses one of these representations. Which of the other two character representations is equivalent to char depends on the compiler.
  • The standard does not define how signed types are represented, but does specify that the range should be evenly divided between positive and negative values. E.g., an 8-bit signed char is guaranteed to be able to hold values from –127 through 127; most modern machines use representations that allow values from –128 through 127.

Advice: Deciding which Type to Use

  • Use an unsigned type when you know that the values cannot be negative.
  • Use int for integer arithmetic. short is too small and long often has the same size as int. If your data values are larger than the minimum guaranteed size of an int, then use long long.
  • Do not use plain char or bool in arithmetic expressions. Use them only to hold characters or truth values. Computations using char are problematic because char is signed on some machines and unsigned on others. If you need a tiny integer, specify either signed char or unsigned char.
  • Use double for floating-point computations; float usually does not have enough precision, and the cost of double-precision calculations versus single-precision is negligible. The precision offered by long double usually is unnecessary and often entails considerable run-time cost.

Exercises Section 2.1.1

Exercise 2.1

What are the differences between int, long, long long, and short? Between an unsigned and a signed type? Between a float and a double?

  • C++ guarantees short and int is at least 16 bits, long at least 32 bits, long long at least 64 bits.
  • The signed can represent positive numbers, negative numbers and zero, while unsigned can only represent numbers no less than zero.
  • The C and C++ standards do not specify the representation of float, double and long double. It is possible that all three implemented as IEEE double‐precision. For most architectures, float is a IEEE single-precision floating point number (binary32), and double is a IEEE double-precision floating point number (binary64).

Exercise 2.2

To calculate a mortgage payment, what types would you use for the rate, principal, and payment? Explain why you selected each type.

  • Use double, or float. The rate like that: 4.50% per year. The principal like that: 854.36.Thepaymentlikethat: 1,142.36

2.1.2. Type Conversions

  • Type conversions happen automatically when we use an object of one type where an object of another type is expected.
#include <iostream>

using std::cout;
using std::endl;

int main()
{
    bool bool1 = -1, bool2 = 0;  // bool1 is true, bool2 is false.
    cout << bool1 << '\t' << bool2 << endl;
    int int1 = bool1, int2 = bool2;  // int1 is 1, int2 is 0.
    cout << int1 << '\t' << int2 << endl;
    int1 = 3.14;  // int1 is 3.
    cout << int1 << endl;
    double double1 = int1;  // double1 is 3.0.
    cout << double1 << endl;
    unsigned char ch1 = -2;  // assuming 8-bit chars, c has value 254.
    signed char ch2 = 256;  // assuming 8-bit chars, the value of ch2 is undefined.
    cout << static_cast<int>(ch1) << '\t' << static_cast<int>(ch2) << endl;

    return 0;
}

/*
Output:
1   0
1   0
3
3
254 0
*/
  1. Assign one of the nonbool arithmetic types to a bool object, the result is false if the value is 0 and true otherwise.
  2. Assign a bool to one of the other arithmetic types, the resulting value is 1 if the bool is true and 0 if the bool is false. When we use a bool in an arithmetic expression, its value converts to either 0 or 1.
  3. Assign a floating-point value to an object of integral type, the value is truncated. The value that is stored is the part before the decimal point.
  4. Assign an integral value to an object of floating-point type, the fractional part is zero. Precision may be lost if the integer has more bits than the floating-point object can accommodate.
  5. Assign an out-of-range value to an object of unsigned type, the result is the remainder of the value modulo(%) the number of values the target type can hold.
    E.g.: An 8-bit unsigned char can hold values from 0 through 255, inclusive. If we assign a value outside this range, the compiler assigns the remainder of that value modulo 256. Therefore, assigning –1 to an 8-bit unsigned char gives that object the value 255.
  6. If we assign an out-of-range value to an object of signed type, the result is undefined.
    • The compiler applies same type conversions when we use a value of one arithmetic type where a value of another arithmetic type is expected. For example, when we use a nonbool value as a condition, the arithmetic value is converted to bool: if the value is 0, then the condition is false; all other nonzero values yield true.
int i = 42;
if(i)  // condition will evaluate as true
    i = 0;

Advice: Avoid Undefined and Implementation-Defined Behavior

  • Undefined behavior results from errors that the compiler is not required and sometimes is not able to detect. Programs that contain undefined behavior can appear to execute correctly in some circumstances but wrong on other circumstances.
  • Programs should also avoid implementation-defined behavior, such as assuming that the size of an int is a fixed and known value. Such programs are said to be nonportable. When the program is moved to another machine, code that relied on implementation-defined behavior may fail.

Expressions Involving Unsigned Types

#include <iostream>

using std::cout;
using std::endl;

int main()
{
    // Test 1:
    cout << "Test 1:\n";
    unsigned int num1 = 10;
    int num2 = -42;
    unsigned num3 = -42;
    cout << num2 + num2 << endl;  // -84
    cout << num1 + num2 << endl;  // 32-bits: 4294967264.
    // num2 first converts to unsigned, the same as num1 + (unsigned)num2
    cout << num1 + num3 << endl;

    // Test 2:
    cout << "\nTest 2:\n";
    unsigned int num4 = 42, num5 = 10;
    cout << num4 - num5 << endl;  // 32
    cout << num5 - num4 << endl;  // The result will wrap around

    // Test 3:
    cout << "\nTest 3:\n";
    for(unsigned num = 3; num >= 0; --num)
    {
        cout << num << endl;
        if(num > 3)
        {
            cout << "Stop: Wrap around!\n";
            break;
        }
    }

    return 0;
}

/*
Output:
Test 1:
-84
4294967264
4294967264

Test 2:
32
4294967264

Test 3:
3
2
1
0
4294967295
Stop: Wrap around!
*/
  1. If we use unsigned and int values in an arithmetic expression, the int value is automatically converted to unsigned.
  2. Regardless of whether one or both operands are unsigned, if we subtract a value from an unsigned, we must be sure that the result cannot be negative.
  3. Consider what happens when u is 0. The result -1 will be transformed to an unsigned value 4294967295. (assuming 32-bit ints)

Caution: Don’t Mix Signed and Unsigned Types

  • Remember signed values are automatically converted to unsigned.
  • a * b, if a is -1 and b is 1, then if both a and b are ints, the value is -1. If a is int and b is an unsigned, then the value depends on how many bits an int has on the particular machine. On our machine, this expression yields 4294967295.

Exercises Section 2.1.2

Exercise 2.3

What output will the following code produce?

unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl;
std::cout << u - u2 << std::endl;
int i = 10, i2 = 42;
std::cout << i2 - i << std::endl;
std::cout << i - i2 << std::endl;
std::cout << i - u << std::endl;
std::cout << u - i << std::endl;
32
4294967264
32
-32
0
0

Exercise 2.4

Write a program to check whether your predictions were correct. If not, study this section until you understand what the problem is.

  • Do it yourself.

2.1.3. Literals字面文字

  • A value, such as 42, is known as a literal because its value self-evident. Every literal has a type which is determined by its form and value.

Integer and Floating-Point Literals

  • We can write an integer literal using decimal, octal, or hexadecimal notation. Integer literals that begin with 0 (zero) are interpreted as octal; begin with 0x/0X are interpreted as hexadecimal.
    20(decimal) = 024(octal) = 0x14(hexadecimal)
  • The type of an integer literal depends on its value and notation. By default:
    1. Decimal literals are signed and a decimal literal has the smallest type of int, long, or long long (i.e., the first type in this list) in which the literal’s value fits.
    2. Octal and hexadecimal literals can be either signed or unsigned types; octal and hexadecimal literals have the smallest type of int, unsigned int, long, unsigned long, long long, or unsigned long long in which the literal’s value fits.
  • It is an error to use a literal that is too large to fit in the largest related type. There are no literals of type short.
  • Although integer literals may be stored in signed types, the value of a decimal literal is never a negative number. -42, the minus sign is not part of the literal, it is an operator that negates the value of its literal operand.
  • Floating-point literals include either a decimal point or an exponent specified using scientific notation. Using scientific notation, the exponent is indicated by either E or e: 3.14159; 3.14159E100
  • By default, floating-point literals have type double. We can override the default using a suffix from Table 2.2 (overleaf).

Character and Character String Literals

  • A character enclosed within single quotes is a literal of type char. Zero or more characters enclosed in double quotation marks is a string literal.
'a'  // character literal
"Hello World!"  // string literal
  • The type of a string literal is array of constant chars(§ 3.5.4 p. 122). The compiler appends a null character (’\0’) to every string literal. Thus, the actual size of a string literal is one more than its apparent size. For example, the literal ‘A’ represents the single character A, whereas the string literal “A” represents an array of two characters, the letter A and the null character.
  • Two string literals that appear adjacent to one another and that are separated only by spaces, tabs, or newlines are concatenated into a single literal.
// multiline string literal
std::cout << "a really, really long string literal "
"that spans two lines" << std::endl;

Escape Sequences

  • Some characters(backspace or control characters) are non-printable, other characters (single and double quotation marks, question mark, and backslash) have special meaning in the language. We use an escape sequence to represent such characters.
  • An escape sequence begins with a backslash. The language defines several escape sequences:
newline         \n  horizontal tab  \t  alert (bell)        \a
vertical tab        \v  backspace       \b  double quote        \"
backslash       \\  question mark   \?  single quote        \'
carriage return \r  form feed       \f
  • We use an escape sequence as if it were a single character:
std::cout << '\n';  // prints a newline
std::cout << "\tHi!\n";  // prints a tab followed by "Hi!" and a newline
  • A generalized escape sequence: \x followed by one or more hexadecimal digits or a \ followed by 1/2/3 octal digits. The value represents the numerical value of the character. Examples (assuming the Latin-1 character set):
    \7 (bell) \12 (newline) \40 (blank) \0 (null) \115 (‘M’) \x4d (‘M’)
  • If a \ is followed by more than three octal digits, only the first three are associated with the . “\1234” represents two characters: the character represented by the octal value 123 and the character 4.

Specifying the Type of a Literal

  • We can override the default type of an integer, floating-point, or character literal by supplying a suffix or prefix as listed in Table 2.2.

L'a'  // wide character literal, type is wchar_t
u8"hi!"  // utf-8 string literal (utf-8 encodes a Unicode character in 8 bits)
42ULL  // unsigned integer literal, type is unsigned long long
1E-3F  // single-precision floating-point literal, type is float
3.14159L  // extended-precision floating-point literal, type is long double

Boolean and Pointer Literals

  • The words true and false are literals of type bool.
  • The word nullptr is a pointer literal(§2.3.2 p.52).

Best Practices

  • When you write a long literal, use the uppercase L; the lowercase letter l is easily mistaken for the digit 1.
  • We can independently specify the signedness and size of an integral literal.
    1. If the suffix contains a U, then the literal has an unsigned type.
    2. If the suffix contains an L, then the literal’s type will be at least long; if the suffix contains LL, then the literal’s type will be either long long or unsigned long long.
    3. We can combine U with either L or LL. For example, a literal with a suffix of UL will be either unsigned long or unsigned long long, depending on whether its value fits in unsigned long.

Exercises Section 2.1.3

Exercise 2.5

Determine the type of each of the following literals. Explain the differences among the literals in each of the four examples:
(a) ‘a’, L’a’, “a”, L”a”
(b) 10, 10u, 10L, 10uL, 012, 0xC
(c) 3.14, 3.14f, 3.14L
(d) 10, 10u, 10., 10e-2

  • (a): character literal, wide character literal, string literal, string wide character literal.
  • (b): decimal, unsigned decimal, long decimal, unsigned long decimal, octal, hexadecimal.
  • (c): double, float, long double.
  • (d): decimal, unsigned decimal, double, double.

Exercise 2.6

What, if any, are the differences between the following definitions:

int month = 9, day = 7;
int month = 09, day = 07;
  • The first line’s integer is decimal.
  • The second line:
    1. int month = 09 is invalid, cause octal don’t have digit 9.
    2. day is octal.

Exercise 2.7

What values do these literals represent? What type does each have?
(a) “Who goes with F\145rgus?\012”
(b) 3.14e1L
(c) 1024f
(d) 3.14L

  • ﴾a﴿: Who goes with Fergus?﴾new line﴿ “string”
  • ﴾b﴿: 31.4 “long double”
  • ﴾c﴿: ERROR: The suffix f is valid only with floating point literals.
double dou = 1024f;  // error: unable to find numeric literal operator ‘operator"" f’
  • ﴾d﴿: 3.14 “long double”

Exercise 2.8

Using escape sequences, write a program to print 2M followed by a newline. Modify the program to print 2, then a tab, then an M, followed by a newline.

#include <iostream>

int main()
{
    std::cout << "\062\115\012";
    std::cout << "\062\t\115\012";
}

/*
Output:
2M
2   M
*/

2.2. Variables

  • A variable provides us with named storage that our programs can manipulate. Each variable in C++ has a type. The type determines the size and layout of the variable’s memory. C++ programmers refer to variables as “variables” or “objects” interchangeably.

2.2.1. Variable Definitions

  • A variable definition consists of a type specifier, followed by a list of one or more variable names separated by commas, and ends with a semicolon.
  • A definition may provide an initial value for one or more of the names it defines:
int sum = 0, value, // sum, value, and units_sold have type int
units_sold = 0; // sum and units_sold have initial value 0
Sales_item item; // item has type Sales_item (see § 1.5.1 (p. 20))
// string is a library type, representing a variable-length sequence of characters
std::string book("0-201-78345-X"); // book initialized from string literal

Terminology: What is an Object?

  • An object is a region of memory that has a type. We will use the term object regardless of whether the object has built-in or class type, is named or unnamed, or can be read or written.

Initializers

  • An object that is initialized gets the specified value at the moment it is created. The values used to initialize a variable can be arbitrarily expressions.
  • When a definition defines two or more variables, the name of each object becomes visible immediately. Thus, it is possible to initialize a variable to the value of one defined earlier in the same definition.
// ok: price is defined and initialized before it is used to initialize discount
double price = 109.99, discount = price * 0.16;
// ok: call applyDiscount and use the return value to initialize salePrice
double salePrice = applyDiscount(price, discount);

Warning

  • Initialization is not assignment.
    1. Initialization happens when a variable is given a value when it is created.
    2. Assignment obliterates an object’s current value and replaces that value with a new one.

List Initialization

  • We can use any of the following four different ways to define an int variable named units_sold and initialize it to 0:
int units_sold = 0;
int units_sold = {0};
int units_sold {0};
int units_sold (0);
  • List initialization(C++11): use curly braces for initialization. Braced lists of initializers can now be used whenever we initialize an object and in some cases when we assign a new value to an object.
  • The compiler will warn us list initialize variables of built-in type if the initializer might lead to the loss of information:
int main()
{
    long double dou = 3.1415926536;
    int num1 {dou};
    int num2 = {dou};
    int num3(dou);
    int num4 = dou;

    return 0;
}
/*
main.cpp: In function ‘int main()’:
main.cpp:4:15: warning: narrowing conversion of ‘dou’ from ‘long double’ to ‘int’ inside { } [-Wnarrowing]
  int num1 {dou};
              ^
main.cpp:5:17: warning: narrowing conversion of ‘dou’ from ‘long double’ to ‘int’ inside { } [-Wnarrowing]
  int num2 = {dou};
               ^
*/
  • More about list Initialization in § 3.2.1 (p. 84) and § 3.3.1 (p. 98).

Default Initialization

  • When we define a variable without an initializer, the variable is default initialized. What that default value is depends on the type of the variable and where the variable is defined.
  • The value of an object of built-in type that is not explicitly initialized depends on where it is defined.
    1. Variables defined outside any function body are initialized to zero.
    2. With one exception in § 6.1.1 (p. 205), variables of built-in type defined inside a function are uninitialized. The value of an uninitialized variable of built-in type is undefined(§ 2.1.2, p. 36). It is an error to copy or try to access the value of a variable whose value is undefined.
  • Each class controls how we initialize objects of that class type. It is up to the class whether we can define objects of that type without an initializer. If we can, the class determines what value the resulting object will have.
  • Most classes let us define objects without explicit initializers. Such classes supply a default value for us. For example, string class says that if we do not supply an initializer, then the resulting string is the empty string:
std::string empty; // empty implicitly initialized to the empty string
Sales_item item; // default-initialized Sales_item object
  • Some classes require that every object be explicitly initialized. The compiler will complain if we try to create an object of such a class with no initializer.
  • Uninitialized objects of built-in type defined inside a function body have undefined value. Objects of class type that we do not explicitly initialize have a value that is defined by the class.

Caution: Uninitialized Variables Cause Run-Time Problems

  • An uninitialized variable has an indeterminate value. Trying to use the value of an uninitialized variable is an error. The compiler is not required to detect such errors, although most will warn about at least some uses of uninitialized variables.
  • What happens when we use an uninitialized variable is undefined. We recommend initializing every object of built-in type.

Exercises Section 2.2.1

Exercise 2.9

Explain the following definitions. For those that are illegal, explain what’s wrong and how to correct it.
(a) std::cin >> int input_value;
(b) int i = { 3.14 };
(c) double salary = wage = 9999.99;
(d) int i = 3.14;

  • error: expected primary-expression before ‘int’. Correct:
    int input_value = 0;
    std::cin >> input_value;
  • warning: narrowing conversion of ‘3.1400000000000001e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]|
  • error: ‘wage’ was not declared in this scope. Correct:
    double wage = 9999.99, salary = wage;
  • No error or warning, but value will be truncated.

Exercise 2.10

What are the initial values, if any, of each of the following variables?

std::string global_str;
int global_int;
int main()
{
    int local_int;
    std::string local_str;
}
  • global_str is global variable, so the value is empty string.
  • global_int is global variable, so the value is zero.
  • local_int is a local variable which is not uninitialized, so it has a undefined value.
  • local_str is also a local variable which is not uninitialized, but it has a value that is defined by the class. So it is empty string.

2.2.2. Variable Declarations and Definitions

  • C++ supports separate compilation which lets us split our programs into several files, each of which can be compiled independently. To support separate compilation, C++ distinguishes between declarations and definitions.
    1. A declaration makes a name known to the program. A file that wants to use a name defined elsewhere includes a declaration for that name. A variable declaration specifies the type and name of a variable.
    2. A definition creates the associated entity. A variable definition is a declaration. In addition to specifying the name and type, a definition also allocates storage and may provide the variable with an initial value.
  • To obtain a declaration that is not a definition, we add “extern” and not provide an explicit initializer:
extern int i; // declares but does not define i
int j; // declares and defines j
  • Any declaration that includes an explicit initializer is a definition. We can provide an initializer on a variable defined as extern, but doing so overrides the extern.
  • It is an error to provide an initializer on an extern inside a function.
extern int num1 = 1;  // Definition. warning: ‘num1’ initialized and declared ‘extern’

int main()
{
    extern int num2 = 2;  // error: ‘num2’ has both ‘extern’ and initializer

    return 0;
}

Note

  • Variables must be defined exactly once but can be declared many times.
  • To use the same variable in multiple files, we must define that variable in one and only one file. Other files that use that variable must declare but not define that variable.
  • More about C++ separate compilation in § 2.6.3 (p.76) and § 6.1.3 (p. 207).

Exercises Section 2.2.2

Exercise 2.11

Explain whether each of the following is a declaration or a definition:
(a) extern int ix = 1024;
(b) int iy;
(c) extern int iz;

  • ﴾a﴿: definition.
  • ﴾b﴿: definition.
  • ﴾c﴿: declaration.

Key Concept: Static Typing

  • C++ is a statically typed language, which means that types are checked at compile time. The process by which types are checked is referred to as type checking.
  • In C++, the compiler checks whether the operations we write are supported by the types we use. If we try to do things that the type does not support, the compiler generates an error message and does not produce an executable file.

2.2.3. Identifiers

  • Identifiers in C++ can be composed of letters, digits, and the underscore character, but they must begin with either a letter or an underscore. The language imposes no limit on name length. Identifiers are case-sensitive: upper and lowercase letters are distinct.
  • The language reserves a set of names for its own use. These names may not be used as identifiers. 2-3 + 2-4

  • The standard also reserves a set of names for use in the standard library.
  • The identifiers we define in our own programs may not contain two consecutive underscores(__), nor can an identifier begin with an underscore followed immediately by an uppercase letter(_A). Identifiers defined outside a function may not begin with an underscore.

Conventions for Variable Names

  1. An identifier should give some indication of its meaning.
  2. Variable names normally are lowercase—index, not Index or INDEX.
  3. Classes name usually begin with an uppercase letter.
  4. Identifiers with multiple words should visually distinguish each word, for example, student_loan or studentLoan, not studentloan.

Exercises Section 2.2.3

Exercise 2.12

Which, if any, of the following names are invalid?
(a) int double = 3.14;
(b) int _;
(c) int catch-22;
(d) int 1_or_2 = 1;
(e) double Double = 3.14;

  • a, c, and d are invalid.

2.2.4. Scope of a Name

  • A scope is a part of the program in which a name has a particular meaning. Most scopes in C++ are delimited by curly braces.
  • The same name can refer to different entities in different scopes. Names are visible from the point where they are declared until the end of the scope in which the declaration appears.
#include <iostream>

int main()
{
    int sum = 0;
    // sum values from 1 through 10 inclusive
    for (int val = 1; val <= 10; ++val)
    {
        sum += val; // equivalent to sum = sum + val
    }
    std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
    return 0;
}
  • Names that are defined outside a function has global scope and they are accessible throughout the program.

Advice: Define Variables Where You First Use Them

  • It is a good idea to define an object near the point at which the object is first used. Doing so improves readability by making it easy to find the definition of the variable.

Nested Scopes

  • Scopes can contain other scopes. The contained (or nested) scope is referred to as an inner scope, the containing scope is the outer scope.
  • Once a name has been declared in a scope, that name can be used by scopes nested inside that scope. Names declared in the outer scope can also be redefined in an inner scope.
#include <iostream>

using std::cout;
using std::endl;

int reused = 100;

int main()
{
    // Output #1
    cout << reused << endl;
    double reused = 3.14;
    // Output #2
    cout << reused << endl;
    // Output #3
    cout << ::reused << endl;
}
/*
Output:
100
3.14
100
*/
  • Output #1 appears before the local definition of reused. This output statement uses the name reused that is defined in the global scope.
  • Output #2 occurs after the local definition of reused. The local reused is now in scope. Thus, this output uses the local object named reused rather than the global one.
  • Output #3 uses the scope operator (§1.2, p. 8) to override the default scoping rules. The global scope has no name. When the scope operator has an empty left-hand side, it is a request to fetch the name on the right-hand side from the global scope.

Warning

  • It is almost always a bad idea to define a local variable with the same name as a global variable that the function uses or might use.

Exercises Section 2.2.4

Exercise 2.13

What is the value of j in the following program?

int i = 42;
int main()
{
    int i = 100;
    int j = i;
}
  • 100

Exercise 2.14

Is the following program legal? If so, what values are printed?

int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
    sum += i;
std::cout << i << " " << sum << std::endl;
  • Legal: 100 45

2.3. Compound Types

  • A compound type is a type that is defined in terms of another type. C++ has several compound types, two of which—references and pointers—will cover in this chapter.

2.3.1. References

  • C++11 introduced a new kind of reference: rvalue reference, § 13.6.1 (p. 532). These references are intended for use inside classes. When we use the term reference, we mean “lvalue reference.”
  • A reference type “refers to” another type. We define a reference type by writing a declarator of the form &d, where d is the name being declared.
  • Ordinarily, when we initialize a variable, the value of the initializer is copied into the object we are creating. When we define a reference, instead of copying the initializer’s value, we bind the reference to its initializer. Once initialized, a reference remains bound to its initial object and there is no way to rebind a reference to refer to a different object, so references must be initialized.
int num1 = 10, num2 = 20;
int &ref1 = num1;
/* No way to rebind a reference to refer to a different object. */
//&ref1 = num2;  // error: lvalue required as left operand of assignment
/* References must be initialized. */
//int &ref2;  // error: ‘ref2’ declared as reference but not initialized

A Reference Is an Alias

  • A reference is not an object, it is another name for an already existing object. After a reference has been defined, all operations on that reference are operations on the object to which the reference is bound:
int num1 = 10, num2 = 20;
int &ref1 = num1;
ref1 = num2;  // num1 = 20, ref1 = 20.
int num3 = ref1;  // num3 = 20
int &ref2 = ref1;
ref2 = 100;  // num1 = 100, ref1 = 100, ref2 = 100.
  • Because references are not objects, we may not define a reference to a reference.
// Do you mean this?
int num = 1;
int &ref1 = num;
//error: cannot declare reference to ‘int&’, which is not a typedef or a template type argument
int& &ref2 = ref1;

Reference Definitions

  • Each identifier that is a reference must be preceded by the & symbol:
int num1 = 1, num2 = 2;
int &ref1 = num1, ref2 = num2;  // ref1 is a reference bound to num1; ref2 is an int.
int num3 = 3, &ref3 = num3;  // num3 is an int; ref3 is a reference bound to num3.
  • With two exceptions in § 2.4.1 (p. 61) and § 15.2.3 (p. 601), the type of a reference and the object to which the reference refers must match exactly. A reference must be bound only to an object, not to a literal or to the result of a more general expression:
double dou = 3.14;
// error: invalid initialization of reference of type ‘int&’ from type ‘double’
int &ref1 = dou;
// error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
int &ref1 = 10;

Exercises Section 2.3.1

Exercise 2.15

Which of the following definitions, if any, are invalid? Why?

(a) int ival = 1.01;
(b) int &rval1 = 1.01;
(c) int &rval2 = ival;
(d) int &rval3;
  • ﴾a﴿: Valid.
  • ﴾b﴿: Invalid. Initializer must be an object.
  • ﴾c﴿: Valid.
  • ﴾d﴿: Invalid. A reference must be initialized.

Exercise 2.16

Which, if any, of the following assignments are invalid? If they are valid, explain what they do.

int i = 0, &r1 = i;
double d = 0, &r2 = d;
(a) r2 = 3.14159;
(b) r2 = r1;
(c) i = r2;
(d) r1 = d;
  • ﴾a﴿: Valid. d = 3.14159.
  • ﴾b﴿: Valid. Automatic convert will happen.
  • ﴾c﴿: Valid, but value will be truncated.
  • ﴾d﴿: Valid, but value will be truncated.
#include <iostream>

using std::cout;

int main()
{
    int i = 0, &r1 = i;
    double d = 0, &r2 = d;
    cout << i << '\t' << r1 << '\t' << d << '\t' << r2 << '\n';

    r2 = 3.14159;
    cout << i << '\t' << r1 << '\t' << d << '\t' << r2 << '\n';

    r2 = r1;
    cout << i << '\t' << r1 << '\t' << d << '\t' << r2 << '\n';

    i = r2;
    cout << i << '\t' << r1 << '\t' << d << '\t' << r2 << '\n';

    r1 = d;
    cout << i << '\t' << r1 << '\t' << d << '\t' << r2 << '\n';

    return 0;
}
/*
Output:
0       0       0       0
0       0       3.14159 3.14159
0       0       0       0
0       0       0       0
0       0       0       0
*/

Exercise 2.17

What does the following code print?

int i, &ri = i;
i = 5;
ri = 10;
std::cout << i << " " << ri << std::endl;
  • 10 10

2.3.2. Pointers

  • A pointer holds the address of another object and we get the address of an object by using the address-of operator(&). We define a pointer type by writing a declarator of the form d, where d is the name being defined. The must be repeated for each pointer variable.
  • A pointer need not be initialized at the time it is defined, and if they are not initialized, they will have undefined value.
  • A pointer can point to several different objects over its lifetime.
  • Because references are not objects, they don’t have addresses. Hence, we may not define a pointer to a reference.
#include <iostream>

using std::cout;

int main()
{
    int num1 = 1, &ref1 = num1;
    int *ptr1 = &num1;
    //int *ptr2 = ref1;  // error: invalid conversion from ‘int’ to ‘int*’
    //int *ptr2 = &&ref1;  // error: invalid conversion from ‘void*’ to ‘int*’
    int *ptr2 = &ref1;

    cout << "Address:\t" << &num1 << '\t' << &ref1 << '\t' << ptr1 << '\t' << ptr2 << '\n';
    cout << "Value:\t\t" << num1 << '\t' << ref1 << '\t' << *ptr1 << '\t' << *ptr2 << '\n';
    // Change object's value:
    num1 = 2;
    cout << "Address:\t" << &num1 << '\t' << &ref1 << '\t' << ptr1 << '\t' << ptr2 << '\n';
    cout << "Value:\t\t" << num1 << '\t' << ref1 << '\t' << *ptr1 << '\t' << *ptr2 << '\n';

    return 0;
}
/*
Output:
Address:    0x7ffd64151e84  0x7ffd64151e84  0x7ffd64151e84  0x7ffd64151e84
Value:      1   1   1   1
Address:    0x7ffd64151e84  0x7ffd64151e84  0x7ffd64151e84  0x7ffd64151e84
Value:      2   2   2   2
*/
  • With two exceptions in §2.4.2 (p. 62) and §15.2.3 (p. 601), the types of the pointer and the object to which it points must match.
double dou = 0.618;
double *ptr1 = &dou;
double *ptr2 = ptr1;
//int *ptr3 = ptr2;  // error: cannot convert ‘double*’ to ‘int*’ in initialization
//int *ptr3 = &dou;  // error: cannot convert ‘double*’ to ‘int*’ in initialization

Pointer Value

  • The value (i.e., the address) stored in a pointer can be in one of four states:
    1. It can point to an object.
    2. It can point to the location just immediately past the end of an object.
    3. It can be a null pointer, indicating that it is not bound to any object.
    4. It can be invalid; values other than the preceding three are invalid.
  • It is an error to copy or try to access the value of an invalid pointer. The result of accessing an invalid pointer is undefined.
  • Because pointers in cases 2 and 3 do not point to any object, we may not use them to access the supposed object to which the pointer points, otherwise the behavior is undefined.

Using a Pointer to Access an Object

  • When a pointer points to an object, we can use the dereference operator (*) to access that object. Dereferencing a pointer yields the object to which the pointer points. We can assign to that object by assigning to the result of the dereference.

Key Concept: Some Symbols Have Multiple Meanings

int i = 42;
int &r = i; // & follows a type and is part of a declaration; r is a reference
int *p; // * follows a type and is part of a declaration; p is a pointer
p = &i; // & is used in an expression as the address-of operator
*p = i; // * is used in an expression as the dereference operator
int &r2 = *p; // & is part of the declaration; * is the dereference operator
  • In declarations, & and * are used to form compound types.
    In expressions, these same symbols are used to denote an operator.

Null Pointers

  • A null pointer does not point to any object. Code can check whether a pointer is null before attempting to use it.
  • Ways to obtain a null pointer:
int *ptr1 = nullptr;  // C++11
int *ptr2 = 0;
int *ptr2 = NULL;  // defined in <cstdlib>
  • nullptr(C++11) is a literal that can be converted to any pointer type.
  • NULL is a preprocessor variable and the cstdlib header defines it as 0. The preprocessor is a program that runs before the compiler and it replaces the variable by its value. Initializing a pointer to NULL is equivalent to initialize it to 0. Modern C++ programs should avoid using NULL and use nullptr instead.
  • It is illegal to assign an int variable to a pointer, even if the variable’s value is 0.
int zero = 0;
int *ptr1 = zero;  // error: invalid conversion from ‘int’ to ‘int*’

Advice: Initialize all Pointers

  • Under most compilers, when we use an uninitialized pointer, the bits in the memory in which the pointer resides are used as an address. Using an uninitialized pointer is a request to access a supposed object at that supposed location. There is no way to distinguish a valid address from an invalid one from the bits that are in the memory in which the pointer was allocated. So, using an uninitialized pointer always results in a run-time crash.
  • Initialize all Pointers:
    1. Define a pointer only after the object to which it should point has been defined.
    2. If there is no object to bind to a pointer, initialize the pointer to nullptr.

Assignment and Pointers

  • Both pointers and references give indirect access to other objects. The biggest difference in how they do so is that a reference is not an object.
    1. No way to make a reference refer to a different object after defining it.
    2. As with any nonreference variable, we can give the pointer a new value by assigning to it, so making the pointer point to a different object.
pi = &ival; // value in pi is changed; pi now points to ival
*pi = 0; // value in ival is changed; pi is unchanged

Other Pointer Operations

  • If the pointer has a valid value, we can use it in a condition: false when the pointer is 0, otherwise true.
  • We can use the equality (==) or inequality (!=) operators to compare two valid pointers of the same type. The result has type bool. Two pointers are equal if they hold the same address(if 2 pointers are both null, if they address the same object, or if they are both pointers one past the same object) and unequal otherwise.
  • It is possible for a pointer to an object and a pointer one past the end of a different object to hold the same address. Such pointers will compare equal.
  • Because these operations use the value of the pointer, a pointer used in a condition or in a comparison must be a valid pointer. Using an invalid pointer as a condition or in a comparison is undefined.

void* Pointers

  • void* is a pointer type that can hold the address of any object. A void* pointer holds an address, but the type of the object at that address is unknown.
  • We can: compare a void* pointer to another pointer; pass it to or return it from a function; assign it to another void* pointer.
    We can’t: use a void* to operate on the object it addresses(since we don’t know that object’s type and the type determines what operations we can perform on the object).
int num1 = 1;
int *ptr1 = &num1;
void *ptr2 = ptr1;
cout << (ptr1 == ptr2) << '\n';  // Output: 1
//int *ptr3 = ptr2;  // error: invalid conversion from ‘void*’ to ‘int*’
void *ptr3 = ptr2;
//cout << *ptr2;  // error: ‘void*’ is not a pointer-to-object type

Exercises Section 2.3.2

Exercise 2.18

Write code to change the value of a pointer. Write code to change the value to which the pointer points.

#include <iostream>

using std::cout;

int main()
{
    int num1 = 1, num2 = 2;
    int *ptr = &num1;
    cout << num1 << '\t' << num2 << '\t' << ptr << "\t\t" << *ptr << '\n';

    // First: change the value of a pointer:
    ptr = &num2;
    cout << num1 << '\t' << num2 << '\t' << ptr << "\t\t" << *ptr << '\n';

    // Second: change the value to which the pointer points:
    *ptr = 3;
    cout << num1 << '\t' << num2 << '\t' << ptr << "\t\t" << *ptr << '\n';

    return 0;
}
/*
Output:
1   2   0x7ffebbb434e0      1
1   2   0x7ffebbb434e4      2
1   3   0x7ffebbb434e4      3
*/

Exercise 2.19

Explain the key differences between pointers and references.

  • Definition
    1. Pointer is “points to” any other type.
    2. Reference is “another name” of an object.
  • Key Difference:
    1. A reference is another name of an already existing object. A pointer is an object in its own right.
    2. Once initialized, a reference remains bound to its initial object. There is no way to rebind a reference to refer to a different object. A pointer can be assigned and copied.
    3. A reference always get the object to which the reference was initially bound. A single pointer can point to several different objects over its lifetime.
    4. A reference must be initialized. A pointer need not be initialized when it is defined.

Exercise 2.20

What does the following program do?

int i = 42;
int *p1 = &i;
*p1 = *p1 * *p1;
  • i = i * i, so i = 1764.

Exercise 2.21

Explain each of the following definitions. Indicate whether any are illegal and, if so, why.
int i = 0;
(a) double* dp = &i;
(b) int *ip = i;
(c) int *p = &i;

  • ﴾a﴿: illegal, cannot initialize a variable of type double * with an rvalue of type int *
  • ﴾b﴿: illegal, cannot initialize a variable of type int * with an lvalue of type int
  • ﴾c﴿: legal.

Exercise 2.22

Assuming p is a pointer to int, explain the following code:

if (p) // ...
if (*p) // ...
  • if ﴾p﴿ // whether p is nullptr?
  • if ﴾*p﴿ // whether the value pointed by p is zero?

Exercise 2.23

Given a pointer p, can you determine whether p points to a valid object? If so, how? If not, why not?

  • There is no way to distinguish a valid address from an invalid one from the bits that are in the memory in which the pointer was allocated.

Exercise 2.24

Why is the initialization of p legal but that of lp illegal?

int i = 42;
void *p = &i;
long *lp = &i;
  • Because the type void* is a pointer type that can hold the address of any object. But we cannot initialize a variable of type long * with an rvalue of type int *.

2.3.3. Understanding Compound Type Declarations

  • There are 2 common styles used to define multiple variables with pointer or reference type.
    1. Place the type modifier adjacent to the identifier.
      int *p1, *p2; // both p1 and p2 are pointers to int
    2. Place the type modifier with the type but defines only one variable per statement:
int* p1; // p1 is a pointer to int
int* p2; // p2 is a pointer to int

Pointers to Pointers

  • We write * for a pointer to a pointer, ** for a pointer to a pointer to a pointer, and so on:
#include <iostream>

using std::cout;

int main()
{
    int num1 = 1, num2 = 2;
    int *p = &num1;
    int **pp = &p;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n\n";

    // Change num1's value: so change *p( = num1), **pp( = num1)
    num1 = 10;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n\n";

    // Change p's value: so change *p(change object), *pp( = p), **pp(change object)
    p = &num2;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n\n";

    // Change *p: so change num2(= *p), **pp(= *p)
    *p = 20;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n\n";

    // Change *pp: so change p(= *pp), *p(change object), **pp(change object)
    *pp = &num1;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n\n";

    // Change **pp: so change num1(= **pp), *p(= **p)
    **pp = 100;
    cout << "num1:\t" << &num1 << "\t\t" << num1 << '\t'
         << "num2:\t" << &num2 << "\t\t" << num2 << '\n'
         << "p:\t" <<  &p << "\t\t" << p << "\t\t" << *p << '\n'
         << "pp:\t" <<  &pp << "\t\t" << pp << "\t\t" << *pp << "\t\t" << **pp << "\n";

    return 0;
}
/*
Output:
num1:   0x7ffc77760e38      1   num2:   0x7ffc77760e3c      2
p:  0x7ffc77760e40      0x7ffc77760e38      1
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e38      1

num1:   0x7ffc77760e38      10  num2:   0x7ffc77760e3c      2
p:  0x7ffc77760e40      0x7ffc77760e38      10
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e38      10

num1:   0x7ffc77760e38      10  num2:   0x7ffc77760e3c      2
p:  0x7ffc77760e40      0x7ffc77760e3c      2
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e3c      2

num1:   0x7ffc77760e38      10  num2:   0x7ffc77760e3c      20
p:  0x7ffc77760e40      0x7ffc77760e3c      20
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e3c      20

num1:   0x7ffc77760e38      10  num2:   0x7ffc77760e3c      20
p:  0x7ffc77760e40      0x7ffc77760e38      10
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e38      10

num1:   0x7ffc77760e38      100 num2:   0x7ffc77760e3c      20
p:  0x7ffc77760e40      0x7ffc77760e38      100
pp: 0x7ffc77760e48      0x7ffc77760e40      0x7ffc77760e38      100
*/

References to Pointers

  • A reference is not an object and we cannot have a pointer to a reference. Because a pointer is an object, we can define a reference to a pointer:
#include <iostream>

using std::cout;

int main()
{
    int num1 = 1, num2 = 2;
    //int &ref = num1;
    //int *ptr = &&ref;  // error: invalid conversion from ‘void*’ to ‘int*’
    int *ptr = &num1;
    int* &ref = ptr;
    cout << ptr << '\t' << *ptr << '\t' << ref << '\t' << *ref << '\n';

    *ref = 10;
    cout << ptr << '\t' << *ptr << '\t' << ref << '\t' << *ref << '\n';

    ref = &num2;
    cout << ptr << '\t' << *ptr << '\t' << ref << '\t' << *ref << '\n';

    return 0;
}
/*
Output:
0x7fffe8573478  1   0x7fffe8573478  1
0x7fffe8573478  10  0x7fffe8573478  10
0x7fffe857347c  2   0x7fffe857347c  2
*/

Exercises Section 2.3.3

Exercise 2.25

Determine the types and values of each of the following variables.
(a) int* ip, &r = ip;
(b) int i, *ip = 0;
(c) int* ip, ip2;

  • (a): ip is a pointer to int, r is a reference to a pointer which points to int.
  • (b): ip is a valid, null pointer, and i is an int.
  • (c): ip is a pointer to int, and ip2 is an int.

2.4. const Qualifier

  • We can make a variable unchangeable by defining the variable’s type as const. Any attempt to assign to a const variable is an error. A const object must be initialized since we can’t change its value after creating it.
const int kBufSize = 512;
kBufSize = 1024;  // error: assignment of read-only variable ‘kBufSize’
const int kBufSize2;  // error: uninitialized const ‘kBufSize2’

By Default, const Objects Are Local to a File

  • When a const object is initialized from a compile-time constant:
    const int kBufSize = 512;
    the compiler will replace uses of the variable with its corresponding value during compilation. That is, the compiler will generate code using the value 512 in the places that our code uses kBufSize.
  • When we split a program into multiple files, every file that uses the const must have access to its initializer. In order to see the initializer, the variable must be defined in every file that wants to use the variable’s value(§ 2.2.2, p. 45).
  • To support this usage and avoid multiple definitions of the same variable, const variables are defined as local to the file. When we define a const with the same name in multiple files, it is as if we had written definitions for separate variables in each file.
  • Sometimes we have a const variable that we want to share across multiple files but whose initializer is not a constant expression. Instead make the compiler generate a separate variable in each file, we want to define the const in one file, and declare it in the other files that use that object.
  • To define a single instance of a const variable, we use “extern” on both definition and declaration(s):
// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc
  • file_1.cc defines and initializes bufSize. In file_1.h, the extern signifies that bufSize is not local to this file and that its definition will occur elsewhere.

Exercises Section 2.4

Exercise 2.26

Which of the following are legal? For those that are illegal, explain why.
(a) const int buf;
(b) int cnt = 0;
(c) const int sz = cnt;
(d) ++cnt; ++sz;

  • Illegal, buf is uninitialized const.
  • Legal.
  • Legal.
  • Illegal, attempt to write to const object(sz).

2.4.1. References to const

  • A reference to const cannot be used to change the object to which the reference is bound.
const int kNum = 1;
const int &ref1 = kNum;
ref1 = 2;  // error: assignment of read-only reference ‘ref1’
// error: invalid initialization of reference of type ‘int&’ from expression of type ‘const int’
int &ref2 = kNum;

Terminology: const Reference is a Reference to const

  • “reference to const” = “const reference.” But technically speaking, there are no const references since a reference is not an object. Because there is no way to make a reference refer to a different object, so all references are const. Whether a reference refers to a const or nonconst type affects what we can do with that reference, not whether we can alter the binding of the reference itself.

Initialization and References to const

  • §2.3.1(p. 51): There are two exceptions to the rule that the type of a reference must match the type of the object to which it refers. The first exception is that we can initialize a reference to const from any expression that can be converted(§ 2.1.2, p. 35) to the type of the reference.
double dval = 3.14;
const int &ri = dval;
  • Operations on ri will be integer operations, but dval is a floating-point number. To ensure that the object to which ri is bound is an int, the compiler transforms this code into something like
const int temp = dval; // create a temporary const int from the double
const int &ri = temp; // bind ri to that temporary
  • ri is bound to a temporary object. A temporary object is an unnamed object created by the compiler when it needs a place to store a result from evaluating an expression.
#include <iostream>

using std::cout;

int main()
{
    int num = 1;
    const int &ref1 = num;
    const int &ref2 = 2;
    const int &ref3 = ref1 * 3;
    double dou = 6.18;
    const int &ref4 = dou;
    cout << &ref1 << '\t' << ref1 << '\n'
         << &ref2 << '\t' << ref2 << '\n'
         << &ref3 << '\t' << ref3 << '\n'
         << &ref4 << '\t' << ref4 << '\n';
    // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    //int &ref4 = 4;

    return 0;
}
/*
Output:
0x7ffe6b2e6c38  1
0x7ffe6b2e6c3c  2
0x7ffe6b2e6c40  3
0x7ffe6b2e6c44  6
*/

A Reference to const May Refer to an Object That Is Not const

  • A reference to const restricts only what we can do through that reference. The underlying object might be nonconst and we can change it by other means:
#include <iostream>

using std::cout;

int main()
{
    int num = 1;
    const int &ref1 = num;
    cout << &num << '\t' << num << '\t' << &ref1 << '\t' << ref1 << '\n';

    //ref1 = 2;  // error: assignment of read-only reference ‘ref1’

    // Change itself directly:
    num = 2;
    cout << &num << '\t' << num << '\t' << &ref1 << '\t' << ref1 << '\n';

    // Change by a non-const reference:
    int &ref2 = num;
    ref2 = 3;
    cout << &num << '\t' << num << '\t' << &ref1 << '\t' << ref1 << '\n';

    // Change by pointer:
    int *ptr = &num;
    *ptr = 4;
    cout << &num << '\t' << num << '\t' << &ref1 << '\t' << ref1 << '\n';

    return 0;
}
/*
Output:
0x7fff58dc1e04  1   0x7fff58dc1e04  1
0x7fff58dc1e04  2   0x7fff58dc1e04  2
0x7fff58dc1e04  3   0x7fff58dc1e04  3
0x7fff58dc1e04  4   0x7fff58dc1e04  4
*/

2.4.2. Pointers and const

  • A pointer to const cannot be used to change the object to which the pointer points. We can only store the address of a const object in a pointer to const.
const int num = 1;
const int *ptr1 = &num;
*ptr1 = 2;  // error: assignment of read-only location ‘*ptr1’
int *ptr2 = &num;  // error: invalid conversion from ‘const int*’ to ‘int*’const 
  • §2.3.2 (p. 52): Two exceptions to the rule that the types of a pointer and the object to which it points must match. The first exception is that we can use a pointer to const to point to a nonconst object. A nonconst object pointed to by a pointer to const can change but not by this pointer.
#include <iostream>

using std::cout;

int main()
{
    int num = 1;
    const int *ptr1 = &num;
    cout << &num << '\t' << num << '\t' << ptr1 << '\t' << *ptr1 << '\n';

    //*ptr1 = 2;  // error: assignment of read-only location ‘* ptr1’

    num = 2;
    cout << &num << '\t' << num << '\t' << ptr1 << '\t' << *ptr1 << '\n';

    int *ptr2 = &num;
    *ptr2 = 3;
    cout << &num << '\t' << num << '\t' << ptr1 << '\t' << *ptr1 << '\n';

    return 0;
}
/*
Output:
0x7fff65ca27cc  1   0x7fff65ca27cc  1
0x7fff65ca27cc  2   0x7fff65ca27cc  2
0x7fff65ca27cc  3   0x7fff65ca27cc  3
*/
  • Difference between reference to const and pointers to const:
#include <iostream>

using std::cout;

int main()
{
    // First: both references to const and pointers to const can refer/point
    // to the non-const same base type object:
    int num1 = 1;
    const int &ref1 = num1;  // ok
    const int *ptr1 = &num1;  // ok

    // Second: references to const can refer to the non-const different base type
    // which can be converted to; pointer to const can't.
    long num2 = 2;
    double num3 = 3.456;
    std::string str = "gaoxiangnumber1";

    const int &ref2 = num2;  // ok
    const int &ref3 = num3;  // ok
    // error: invalid initialization of reference of type ‘const int&’ from type ‘std::string {aka std::basic_string<char>}’
    const int &ref4 = str;
     const int &ref5 = 0;  // ok

    // error: cannot convert ‘long int*’ to ‘const int*’ in initialization
    const int *ptr2 = &num2;
    // error: cannot convert ‘double*’ to ‘const int*’ in initialization
    const int *ptr3 = &num3;
    const int *ptr4 = 1;  // error: invalid conversion from ‘int’ to ‘const int*’
    const int *ptr5 = &ref5;  // ok

    return 0;
}

const Pointers

  • Since pointers are objects, we can have a pointer that is itself const. A const pointer must be initialized, and its value(i.e., the address that it holds) cannot be changed after initialized. We indicate that the pointer is const by putting the const after the *.
#include <iostream>

using std::cout;

int main()
{
    int num1 = 1, num2 = 2;
    int *ptr1 = &num1;  // a nonconst pointer that points to nonconst
    int *const ptr2 = &num1;  // a const pointer that points to nonconst
    const int *ptr3 = &num1;  // a nonconst pointer that points to const
    const int *const ptr4 = &num1;  // a const pointer that points to const

    ptr1 = &num2;
    *ptr1 = 3;

    //ptr2 = &num2;  // error: assignment of read-only variable ‘ptr2’
    *ptr2 = 4;

    ptr3 = &num2;
    //*ptr3 = 5;  // error: assignment of read-only location ‘* ptr3’

    //ptr4 = &num2;  // error: assignment of read-only variable ‘ptr4’
    //*ptr4 = 6;  // error: assignment of read-only location ‘*(const int*)ptr4’

    return 0;
}
TypeCan change pointer itself?Can change *pointer value?
int *YesYes
int *constNoYes
const int *YesNo
const int *constNoNo

Exercises Section 2.4.2

Exercise 2.27

Which of the following initializations are legal? Explain why.
(a) int i = -1, &r = 0;
(b) int *const p2 = &i2;
(c) const int i = -1, &r = 0;
(d) const int *const p3 = &i2;
(e) const int *p1 = &i2;
(f) const int &const r2;
(g) const int i2 = i, &r = i;

  • Illegal, r must refer to an object.
  • Legal.
  • Legal.
  • Legal.
  • Legal.
  • Illegal, r2 is a reference that cannot be const.
  • Legal.

Exercise 2.28

Explain the following definitions. Identify any that are illegal.
(a) int i, *const cp;
(b) int *p1, *const p2;
(c) const int ic, &r = ic;
(d) const int *const p3;
(e) const int *p;

  • Illegal, cp must initialize.
  • Illegal, p2 must initialize.
  • Illegal, ic must initialize.
  • Illegal, p3 must initialize.
  • Legal.

Exercise 2.29

Using the variables in the previous exercise, which of the following assignments are legal? Explain why.
(a) i = ic;
(b) p1 = p3;
(c) p1 = ⁣
(d) p3 = ⁣
(e) p2 = p1;
(f) ic = *p3;

  • Legal.
  • Illegal. p3 is a pointer to const int.
  • Illegal. ic is a const int.
  • Illegal. p3 is a const pointer.
  • Illegal. p2 is a const pointer.
  • Illegal. ic is a const int.

2.4.3. Top-Level const

  • Top-level const indicates that an object itself is const(E.g.: the pointer itself is a const). Top-level const can appear in any object type(one of the built-in arithmetic types, a class type, or a pointer type).
  • Low-level const appears in the base type of compound types such as pointers(the pointer point to a const object) or references.
  • Pointer types can have both top-level and low-level const independently.
    Const in reference types is low-level.
int i = 0;
int *const p1 = &i; // top-level
const int ci = 42; // top-level
const int *p2 = &ci; // low-level
const int *const p3 = p2; // right-most const is top-level, left-most is low-level
const int &r = ci; // low-level
  • When we copy an object, top-level consts are ignored, low-level const is never ignored. Both objects must have the same low-level const qualification or there must be a conversion between the types of the two objects when we copy an object. We can convert a nonconst to const but not the other way round.
const int num1 = 1;
int num2 = num1;  // ok: copying the value of num1; top-level const in num1 is ignored
const int *const ptr1 = &num1;
const int *ptr2 = ptr1;  // ok: pointed-to type matches; top-level const in ptr1 is ignored
int *ptr3 = ptr2;  // error: ptr2 has a low-level const but ptr3 doesn't

Exercises Section 2.4.3

Exercise 2.30

For each of the following declarations indicate whether the object being declared has top-level or low-level const.

const int v2 = 0;
int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;
objectTop-levelLow-level
v2YesNo
v1NoNo
p1NoNo
r1YesNo
p2NoYes
p3YesYes
r2YesYes

Exercise 2.31

Given the declarations in the previous exercise determine whether the following assignments are legal. Explain how the top-level or low-level const applies in each case.

r1 = v2;
p1 = p2;
p2 = p1;
p1 = p3;
p2 = p3;
  • Legal, top-level const in v2 is ignored.
  • Illegal, p2 has a low-level const but p1 doesn’t.
  • Legal, we can convert int* to const int*.
  • Illegal, p3 has a low-level const but p1 doesn’t.
  • Legal, p2 has the same low-level const qualification as p3.

2.4.4. constexpr and Constant Expressions

  • A constant expression is an expression whose value can be evaluated at compile time and cannot change, such as: a literal, a const object that is initialized from a constant expression.
  • Whether a given object or expression is a constant expression depends on the types and the initializers. For example:
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression because it is a plain int.
const int sz = get_size();
// sz is not a c-e since the value of its initializer is not known until run time

constexpr Variables(C++11)

  • We can ask the compiler to verify that a variable is a constant expression by declaring the variable in a constexpr declaration. Variables declared as constexpr are implicitly const and must be initialized by constant expressions.
constexpr int num = 20;  // 20 is a constant expression
constexpr int limit = num + 1;  // num + 1 is a constant expression
num = 1;  // error: assignment of read-only variable ‘num’
constexpr int sz = size();  // ok only if size is a constexpr function
  • We cannot use an ordinary function as an initializer for a constexpr variable. §6.5.2 (p. 239): C++11 lets us define functions as constexpr. Such functions must be simple enough that the compiler can evaluate them at compile time. We can use constexpr functions in the initializer of a constexpr variable.

Literal Types

  • The types we can use in a constexpr are known as “literal types” because they are simple enough to have literal values.
  • The arithmetic, reference, and pointer types are literal types. Our Sales_item class and the library IO and string types are not literal types. Other literal types in §7.5.6 (p. 299) and §19.3 (p. 832).
  • We can initialize a constexpr pointer from the nullptr literal or the literal 0. We can also point to (or bind to) an object that remains at a fixed address.
  • §6.1.1 (p. 204): Objects defined inside a function are not stored at a fixed address; objects defined outside of any function are stored at a fixed address. Functions may define variables that exist across calls to that function and these local objects also have fixed addresses.
int g_num = 2;

int main()
{
    constexpr int *ptr1 = nullptr;
    constexpr int *ptr2 = 0;

    int num = 1;
    constexpr int *ptr3 = &num;  // error: ‘& num1’ is not a constant expression
    constexpr int &ref1 = num;  // error: ‘* & num1’ is not a constant expression
    constexpr const int &ref2 = 1;
    // error: ‘* &<anonymous>’ is not a constant expression

    constexpr int *ptr4 = &g_num;  // ok
    constexpr int &ref3 = g_num;  // ok
}

Pointers and constexpr

  • constexpr defines a top-level const on the objects it defines. A constexpr pointer is a const pointer that points to a const or a nonconst type.
int g_num1 = 1, g_num2 = 2;
const int g_num3 = 3;

int main()
{
    constexpr int *ptr1 = &g_num1;  // ptr1 is "int *const"
    *ptr1 = 4;  // ok, g_num1 = 4
    ptr1 = &g_num2;  // error: assignment of read-only variable ‘ptr1’

    constexpr const int *ptr2 = &g_num3;  // ptr2 is "const int *const"
    constexpr const int *ptr3 = &g_num1;  // ptr3 can point to nonconst.

    return 0;
}

Exercises Section 2.4.4

Exercise 2.32

Is the following code legal or not? If not, how might you make it legal?
int null = 0, *p = null;

  • Illegal. int null = 0, *p = nullptr;

2.5. Dealing with Types

2.5.1. Type Aliases

  • A type alias is a name that is a synonym for another type. We can define a type alias in one of two ways: typedef or using(C++11).
typedef double wages;  // wages is a synonym for double
typedef wages base, *p;  // base is a synonym for double, p for double*
using SI = Sales_item; // SI is a synonym for Sales_item
wages hourly, weekly; // same as double hourly, weekly;
SI item; // same as Sales_item item

Pointers, const, and Type Aliases

typedef  int*  pint;
int num1 = 1, num2 = 2;
const pint ptr = &num1;  // equal to: int *const ptr = &num1;
*ptr = 3;  // ok
ptr = &num2;  // error: assignment of read-only variable ‘ptr’
  • It is wrong to interpret a declaration that uses a type alias by replacing the alias with its corresponding type:
    const int *ptr = &num1; // wrong interpretation

2.5.2. The auto Type Specifier(C++11)

  • C++11: Let the compiler figure out the type by using the auto type specifier. auto tells the compiler to deduce the type from the initializer and an auto variable must have an initializer.
    auto item = val1 + val2; // item initialized to the result of val1 + val2
  • Because a declaration can involve only a single base type, the initializers for all the variables in the declaration must have types that are consistent with each other:
auto num1 = 0, *ptr = &num1;  // ok: num1 is int and ptr is a pointer to int
auto num2 = 1, num3 = 6.18;
// error: inconsistent deduction for ‘auto’: ‘int’ and then ‘double’

Compound Types, const, and auto

  • auto initialization rules.
    1. When we use a reference as an initializer, the compiler uses the corresponding object’s type for auto’s type deduction.
int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
  1. By default, auto ignores top-level consts, keeps low-level consts. If we want to have a top-level const, we must use “const” explicitly.
int num1 = 1, num2 = 2;
const int num3 = 3, &ref = num3;

auto obj1 = num3;  // obj1 is an int (top-level const in num3 is dropped)
obj1 = 4;  // ok

auto obj2 = ref;  // obj2 is an int (ref is an alias for num3 whose const is top-level)
obj2 = 5;  // ok

auto obj3 = &num1;  // obj3 is an int*(& of an int object is int*)
*obj3 = 6;  // ok
obj3 = &num2;  // ok

auto obj4 = &num3;  // obj4 is const int*(& of a const object is low-level const)
*obj4 = 7;  // error: assignment of read-only location ‘* obj4’
obj4 = &num2;  // ok

const auto obj5 = num3;  // deduced type of num3 is int; obj5 has type const int
obj5 = 8;  // error: assignment of read-only variable  ‘obj5’
  1. When we ask for a reference to an auto-deduced type, top-level consts in the initializer are not ignored.
const int ci = 0;
auto &g = ci;  // g is a const int& that is bound to ci
auto &h = 42;  // error: we can't bind a plain reference to a literal
const auto &j = 42;  // ok: we can bind a const reference to a literal
  1. When we define several variables in the same statement, the initializers must provide consistent auto-deduced types:
int num1 = 1;
const int num2 = 2;
auto &ref1 = num2, *ptr1 = &num2;  // ref1 is a const int&; ptr1 is a pointer to const int
// error: inconsistent deduction for ‘auto’: ‘int’ and then ‘const int’
auto &ref2 = num1, *ptr2 = &num2;

Exercises Section 2.5.2

Exercise 2.33

Using the variable definitions from this section, determine what happens in each of these assignments:

a = 42;
b = 42;
c = 42;
d = 42;
e = 42;
g = 42;
  • Set 42 to int a.
  • Set 42 to int b.
  • Set 42 to int c.
  • ERROR, d is an int *. correct: *d = 42;
  • ERROR, e is an const int *. correct: e = &c;
  • ERROR, g is a const int& that is bound to ci.

Exercise 2.34

Write a program containing the variables and assignments from the previous exercise. Print the variables before and after the assignments to check whether your predictions in the previous exercise were correct. If not, study the examples until you can convince yourself you know what led you to the wrong conclusion.

#include <iostream>

int main()
{
    int i = 0, &r = i;
    const int ci = i, &cr = ci;

    auto a = r;  // a is an int (r is an alias for i, which has type int)
    auto b = ci;  // b is an int (top-level const in ci is dropped)
    auto c = cr;  // c is an int (cr is an alias for ci whose const is top-level)
    auto d = &i;  // d is an int* (& ofan int objectis int*)
    auto e = &ci;  // e is const int*(& of a const object is low-level const)
    const auto f = ci;  // deduced type of ci is int; f has type const int
    auto &g = ci;  // g is a const int& that is bound to ci

    a = 42;
    b = 42;
    c = 42;
    *d = 42;
    e = &c;

    return 0;
}

Exercise 2.35

Determine the types deduced in each of the following definitions. Once you’ve figured out the types, write a program to see whether you were correct.

const int i = 42;
auto j = i;
const auto &k = i;
auto *p = &i;
const auto j2 = i, &k2 = i;
  • i is const int.
  • j is int.
  • k is const int&.
  • p is const int *.
  • j2 is const int.
  • k2 is const int&.
#include <iostream>
#include <typeinfo>

using std::cout;

int main()
{
    const int i = 42;
    auto j = i;
    const auto &k = i;
    auto *p = &i;
    const auto j2 = i, &k2 = i;

    // i: int; PKi: pointer to const int.
    cout << "j is " << typeid(j).name() << "\nk is " << typeid(k).name()
         << "\np is " << typeid(p).name() << "\nj2 is " << typeid(j2).name()
         << "\nk2 is " << typeid(k2).name() << '\n';

    return 0;
}

/*
Output:
j is i
k is i
p is PKi
j2 is i
k2 is i
*/

2.5.3. The decltype Type Specifier(C++11)

  • decltype returns the type of its operand. The compiler analyzes the expression to determine its type but does not evaluate the expression:
    decltype(f()) sum = x; // sum has type f returns
    The compiler does not call f, but it uses the type that f return as the type for sum.
  • When we apply a variable to decltype, decltype returns the type of that variable, including top-level const and references.
const int num1 = 1, &ref1 = num1;
decltype(num1) obj1 = 0;  // obj1 has type const int
obj1 = 1;  // error: assignment of read-only variable ‘obj1’
decltype(ref1) obj2 = obj1;  // obj2 has type const int& and is bound to obj1
obj2 = 2;  // error: assignment of read-only reference ‘obj2’

decltype and References

  • When we apply decltype to an expression that is not a variable, we get the type that the expression yields. decltype returns a reference type for expressions that yield objects that can stand on the left-hand side of the assignment.
int num1 = 1, *ptr1 = &num1, &ref1 = num1;
// obj1 is "int &". error: ‘obj1’ declared as reference but not initialized
decltype(ref1) obj1;
decltype(ref1 + 2) obj2;  // obj2 is "int", addition yields an int.
obj2 = 2;  // ok, assignment
// obj3 is "int &". error: ‘obj3’ declared as reference but not initialized
decltype(*ptr1) obj3;

const int num2 = 3, *ptr2 = &num2, &ref2 = num2;
decltype(ref2) obj4 = num1;  // obj4 is "const int &"
obj4 = 4;  // error: assignment of read-only reference ‘obj4’
decltype(ref2 + 1) obj5 = 5;  // ok: obj5 is an int
decltype(*ptr1) obj6 = num1;  // obj6 is "int &"
obj6 = 5;  // ok
// error: invalid initialization of reference of type ‘int&’ from expression of type ‘const int’
decltype(*ptr1) obj7 = num2;  // obj7 is also "int &"
refint &const int &
decltype(ref)int &const int &
ptrint *const int *
decltype(*ptr)int &int &

- decltype(ref1) is a reference type. If we want the type to which ref1 refers, we can use ref1 in an expression, such as r + 0, which is an expression that yields a value that has a nonreference type.
- When we dereference a pointer, we get the object to which the pointer points. So, the type deduced by decltype(*ptr) is int&.
- Note: When we apply decltype to a variable without any parentheses “()”, we get the type of that variable. If we wrap the variable’s name in one or more sets of parentheses, the compiler will evaluate the operand as an expression. In other words, decltype((variable)) is a reference type, but decltype(variable) is a reference type only if variable is a reference.

int num1 = 1;
decltype(num1) obj1 = 3;  // obj1 is "int"
decltype((num1)) obj2 = num1;  // obj2 is "int &"
obj2 = 4;  // ok

const int num2 = 2;
decltype(num2) obj3 = 5;  // obj3 is "const int"
obj3 = 6;  // error: assignment of read-only variable ‘obj3’
decltype((num2)) obj4 = num2;  // obj4 is "const int &"
obj4 = 7;  // error: assignment of read-only reference ‘obj4’

Exercises Section 2.5.3

Exercise 2.36

In the following code, determine the type of each variable and the value each variable has when the code finishes:

int a = 3, b = 4;
decltype(a) c = a;
decltype((b)) d = a;
++c;
++d;
  • c is int; d is int &. Result: a = 4, b = 4, c = 4, d = 4.

Exercise 2.37

Assignment is an example of an expression that yields a reference type. The type is a reference to the type of the left-hand operand. That is, if i is an int, then the type of the expression i = x is int&. Using that knowledge, determine the type and value of each variable in this code:

int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;
  • c is int, d is int &. Result: a = 3, b = 4, c = 3, d = 3.
    Note: decltype doesn’t evaluate the expression, so a is still 3.

Exercise 2.38

Describe the differences in type deduction between decltype and auto. Give an example of an expression where auto and decltype will deduce the same type and an example where they will deduce differing types.

2.6. Defining Our Own Data Structures

  • In C++ we define our own data types by defining a class.

2.6.1. Defining the Sales_data Type

struct Sales_data
{
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
  • struct {}; The close curly that ends the class body must be followed by a semicolon. The semicolon is needed because we can define variables after the class body.
struct Sales_data { /* ... */ } accum, trans, *salesptr;
// equivalent, but better way to define these objects
struct Sales_data { /* ... */ };
Sales_data accum, trans, *salesptr;

Class Data Members

  • The class body defines the members of the class. The data members of a class define the contents of the objects of that class type. Each object has its own copy of the class data members.
  • C++11: We can supply an in-class initializer for a data member. When we create objects, the in-class initializers will be used to initialize the data members. Members without an initializer are default initialized(§ 2.2.1, p. 43). Thus, units_sold and revenue will be initialized to 0, and bookNo will be initialized to the empty string.
  • In-class initializers are restricted as to the form(§ 2.2.1, p. 43) we can use: They must either be enclosed inside curly braces or follow an = sign. We may not specify an in-class initializer inside parentheses.

Exercises Section 2.6.1

Exercise 2.39

Compile the following program to see what happens when you forget the semicolon after a class definition. Remember the message for future reference.

struct Foo { /* empty */ } // Note: no semicolon
int main()
{
    return 0;
}
  • error: expected ‘;’ after struct definition

Exercise 2.40

Write your own version of the Sales_data class.

struct Sale_data
{
    std::string bookNo;
    std::string bookName;
    unsigned units_sold = 0;
    double revenue = 0.0;
    double price = 0.0;
    //...
};

2.6.2. Using the Sales_data Class

Adding Two Sales_data Objects

#include <iostream>
#include <string>
#include "Sales_data.h"
int main()
{
    Sales_data data1, data2;
    // code to read into data1 and data2
    // code to check whether data1 and data2 have the same ISBN
    // and if so print the sum of data1 and data2
}

Reading Data into a Sales_data Object

  • The string type holds a sequence of characters. Its operations include the >>, <<, and == operators to read, write, and compare strings, respectively.
double price = 0; // price per book, used to calculate total revenue
// read the first transactions: ISBN, number of books sold, price per book
std::cin >> data1.bookNo >> data1.units_sold >> price;
// calculate total revenue from price and units_sold
data1.revenue = data1.units_sold * price;
// read the second transaction
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;

Printing the Sum of Two Sales_data Objects

if (data1.bookNo == data2.bookNo)
{
    unsigned totalCnt = data1.units_sold + data2.units_sold;
    double totalRevenue = data1.revenue + data2.revenue;
    // print: ISBN, total sold, total revenue, average price per book
    std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
    if (totalCnt != 0)
    {
        std::cout << totalRevenue/totalCnt << std::endl;
    }
    else
    {
        std::cout << "(no sales)" << std::endl;
    }
    return 0;  // indicate success
}
else  // transactions weren't for the same ISBN
{
    std::cerr << "Data must refer to the same ISBN" << std::endl;
    return -1;  // indicate failure
}

Exercises Section 2.6.2

Exercise 2.41

Use your Sales_data class to rewrite the exercises in §1.5.1 (p. 22), § 1.5.2 (p. 24), and § 1.6 (p. 25). For now, you should define your Sales_data class in the same file as your main function.

/****************************** 1.5.1 ******************************/
#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;

struct Sale_data
{
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data book;
    double price;
    cin >> book.bookNo >> book.units_sold >> price;
    book.revenue = book.units_sold * price;
    cout << book.bookNo << " " << book.units_sold << " " << book.revenue << " " << price;

    return 0;
}
/****************************** 1.5.2 ******************************/
#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::cerr;
using std::endl;

struct Sale_data
{
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data book1, book2;
    double price1, price2;
    cin >> book1.bookNo >> book1.units_sold >> price1;
    cin >> book2.bookNo >> book2.units_sold >> price2;
    book1.revenue = book1.units_sold * price1;
    book2.revenue = book2.units_sold * price2;

    if(book1.bookNo == book2.bookNo)
    {
        unsigned totalCnt = book1.units_sold + book2.units_sold;
        double totalRevenue = book1.revenue + book2.revenue;
        cout << book1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
        if (totalCnt != 0)
        {
            cout << totalRevenue / totalCnt << endl;
        }
        else
        {
            cout << "(no sales)" << endl;
        }
        return 0;
    }
    else
    {
        cerr << "Data must refer to same ISBN" << endl;
        return -1; // indicate failure
    }
}
/****************************** 1.6 ******************************/
#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::cerr;
using std::endl;

struct Sale_data
{
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data total;
    double totalPrice;

    if (cin >> total.bookNo >> total.units_sold >> totalPrice)
    {
        total.revenue = total.units_sold * totalPrice;
        Sale_data trans;
        double transPrice;
        while (cin >> trans.bookNo >> trans.units_sold >> transPrice)
        {
            trans.revenue = trans.units_sold * transPrice;
            if (total.bookNo == trans.bookNo)
            {
                total.units_sold += trans.units_sold;
                total.revenue += trans.revenue;
            }
            else
            {
                cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " ";
                if (total.units_sold != 0)
                {
                    cout << total.revenue / total.units_sold << endl;
                }
                else
                {
                    cout << "(no sales)" << endl;
                }
                total.bookNo = trans.bookNo;
                total.units_sold = trans.units_sold;
                total.revenue = trans.revenue;
            }
        }
        cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " ";
        if (total.units_sold != 0)
        {
            cout << total.revenue / total.units_sold << endl;
        }
        else
        {
            cout << "(no sales)" << endl;
        }
        return 0;
    }
    else
    {
        cerr << "No data?!" << endl;
        return -1;  // indicate failure
    }
}

2.6.3. Writing Our Own Header Files

  • Classes usually are stored in headers whose name derives from the name of the class.
  • Headers usually contain entities(such as class definitions, const and constexpr variables(§ 2.4, p. 60)) that can be defined only once in any given file.

A Brief Introduction to the Preprocessor

  • Common technique for making it safe to include a header multiple times relies on the preprocessor which is a program that runs before the compiler and changes the source text of our programs.
  • When the preprocessor sees a #include, it replaces the #include with the contents of the specified header.
  • C++ use the preprocessor to define header guards. Header guards rely on preprocessor variables(§ 2.3.2, p. 53) which have one of two possible states(defined or not defined).
  • The #define directive takes a name and defines that name as a preprocessor variable. There are two other directives that test whether a given preprocessor variable has or has not been defined: #ifdef is true if the variable has been defined, and #ifndef is true if the variable has not been defined. If the test is true, then everything following the #ifdef or #ifndef is processed up to the matching #endif.
  • We can use these facilities to guard against multiple inclusion as follows.
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data
{
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
#endif
  • The first time Sales_data.h is included, the #ifndef test will succeed. The preprocessor will process the lines following #ifndef up to the #endif. So, the preprocessor variable SALES_DATA_H will be defined and the contents of Sales_data.h will be copied into our program. If we include Sales_data.h later on in the same file, the #ifndef directive will be false. The lines between it and the #endif directive will be ignored.
  • Preprocessor variables, including names of header guards, must be unique throughout the program and they are usually written in all uppercase.

Best Practices

  • Headers should have guards, even if they aren’t included by another header..

Exercises Section 2.6.3

Exercise 2.42

Write your own version of the Sales_data.h header and use it to rewrite the exercise from § 2.6.2 (p. 76).

/****************************** Sales_Data ******************************/
#ifndef SALES_DATA_H
#define SALES_DATA_H

#include <string>
#include <iostream>

using std::cin;
using std::cout;
using std::cerr;
using std::endl;
using std::string;

struct Sales_data
{
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;

    void CalcRevenue(double price);
    double CalcAveragePrice();
    void SetData(Sales_data data);
    void AddData(Sales_data data);
    void Print();
};

void Sales_data::CalcRevenue(double price)
{
    revenue = units_sold * price;
}

void Sales_data::SetData(Sales_data data)
{
    bookNo = data.bookNo;
    units_sold = data.units_sold;
    revenue = data.revenue;
}

void Sales_data::AddData(Sales_data data)
{
    if (bookNo != data.bookNo)
    {
        return;
    }
    units_sold += data.units_sold;
    revenue += data.revenue;
}

double Sales_data::CalcAveragePrice()
{
    if (units_sold != 0)
    {
        return revenue / units_sold;
    }
    else
    {
        return 0.0;
    }
}

void Sales_data::Print()
{
    cout << bookNo << " " << units_sold << " " << revenue << " ";
    double averagePrice = CalcAveragePrice();
    if (averagePrice != 0.0)
    {
        cout << averagePrice << endl;
    }
    else
    {
        cout << "(no sales)" << endl;
    }
}

#endif // SALES_DATA_H
/****************************** 1.5.1 ******************************/
#include "Sales_data.h"

int main()
{
    Sales_data book;
    double price;
    cin >> book.bookNo >> book.units_sold >> price;
    book.CalcRevenue(price);
    book.Print();

    return 0;
}
/****************************** 1.5.2 ******************************/
#include "Sales_data.h"

int main()
{
    Sales_data book1, book2;
    double price1, price2;
    cin >> book1.bookNo >> book1.units_sold >> price1;
    cin >> book2.bookNo >> book2.units_sold >> price2;
    book1.CalcRevenue(price1);
    book2.CalcRevenue(price2);

    if (book1.bookNo == book2.bookNo)
    {
        book1.AddData(book2);
        book1.Print();

        return 0;
    }
    else
    {
        cerr << "Data must refer to same ISBN" << endl;
        return -1; // indicate failure
    }
}
/****************************** 1.6 ******************************/
#include "Sales_data.h"

int main()
{
    Sales_data total;
    double totalPrice;
    if (cin >> total.bookNo >> total.units_sold >> totalPrice)
    {
        total.CalcRevenue(totalPrice);
        Sales_data trans;
        double transPrice;

        while (cin >> trans.bookNo >> trans.units_sold >> transPrice)
        {
            trans.CalcRevenue(transPrice);
            if (total.bookNo == trans.bookNo)
            {
                total.AddData(trans);
            }
            else
            {
                total.Print();
                total.SetData(trans);
            }
        }
        total.Print();

        return 0;
    }
    else
    {
        cerr << "No data?!" << endl;
        return -1; // indicate failure
    }
}

Chapter Summary

Please indicate the source: http://blog.youkuaiyun.com/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值