《C++ Primer》导学系列:第 2 章 - 变量和基本类型

2.1 基本内置类型

概述

本小节介绍C++中的基本内置类型。基本内置类型是构成C++程序的基础,它们用于表示数据的不同形式,如整数、浮点数、字符等。理解和使用这些基本类型是编写C++程序的起点。

2.1.1 算术类型

C++的算术类型分为整型和浮点型。整型用于表示整数,浮点型用于表示带小数点的数值。

整型

整型包括各种不同的类型,以满足不同范围和精度的需求。

  • 基本整型类型
bool flag = true;            // 布尔类型
char ch = 'a';               // 字符类型
signed char sch = -1;        // 有符号字符类型
unsigned char uch = 255;     // 无符号字符类型
short s = -32768;            // 短整型
unsigned short us = 65535;   // 无符号短整型
int i = -2147483648;         // 整型
unsigned int ui = 4294967295;// 无符号整型
long l = -9223372036854775808L;       // 长整型
unsigned long ul = 18446744073709551615UL; // 无符号长整型
long long ll = -9223372036854775807LL;     // 长长整型
unsigned long long ull = 18446744073709551615ULL; // 无符号长长整型

浮点型

浮点型用于表示带小数点的数值,分为floatdoublelong double三种类型,分别表示单精度、双精度和扩展精度浮点数。

  • 基本浮点类型
float f = 3.14f;           // 单精度浮点型
double d = 3.14;           // 双精度浮点型
long double ld = 3.14L;    // 扩展精度浮点型

2.1.2 类型转换

类型转换用于在不同类型之间进行转换。C++支持隐式转换和显式转换。

隐式转换

隐式转换在需要时自动进行,不需要显式指定。

  • 示例
int i = 42;
double d = i;  // int隐式转换为double

显式转换

显式转换需要使用强制转换运算符。

  • C风格的显式转换
double d = 3.14;
int i = (int)d;  // double显式转换为int

  • C++风格的显式转换
double d = 3.14;
int i = static_cast<int>(d);  // 使用static_cast进行显式转换

2.1.3 字面值常量

字面值常量是程序中直接使用的常量值,它们在程序运行时是固定不变的。字面值常量可以是整数、浮点数、字符、字符串等。理解字面值常量有助于编写更清晰、可读性更高的代码。

整数字面值

整数字面值表示整数,可以用不同的进制表示,如十进制、八进制和十六进制。

  • 十进制:不带前缀的整数。
int dec = 42;  // 十进制

  • 八进制:以0开头的整数。
int oct = 052;  // 八进制

  • 十六进制:以0x0X开头的整数。
int hex = 0x2A;  // 十六进制

浮点字面值

浮点字面值表示带小数点的数值,可以用科学计数法表示。默认情况下,浮点字面值是double类型,可以通过后缀fF指定为float类型,通过后缀lL指定为long double类型。

  • 普通表示
double d = 3.14;

  • 科学计数法表示
double d2 = 3.14e2;  // 3.14 × 10^2

  • 指定类型
float f = 3.14f;           // float类型
long double ld = 3.14l;    // long double类型

字符和字符串字面值

字符和字符串字面值用于表示字符和字符串。

  • 字符字面值:用单引号括起来的单个字符。
char c = 'a';
char newline = '\n';  // 转义字符

  • 字符串字面值:用双引号括起来的一串字符。
const char* str = "Hello, World!";

布尔字面值和指针字面值

C++中还包括布尔字面值和指针字面值。

  • 布尔字面值truefalse
bool flag = true;

  • 指针字面值nullptr表示空指针。
int* p = nullptr;

重点与难点分析

重点

  1. 基本数据类型:理解整型和浮点型的不同类型及其适用范围。
  2. 类型转换:掌握隐式转换和显式转换的用法,特别是C++风格的显式转换。

难点

  1. 类型转换:初学者容易混淆隐式和显式转换的用法,需要通过练习掌握。

总结与提高

本节总结

  1. 了解了C++中的基本内置类型,包括整型和浮点型。
  2. 学习了类型转换的概念和具体用法,包括隐式转换和显式转换。

提高建议

  1. 多练习基本类型的使用:通过编写各种小程序,熟悉不同基本类型的定义和操作。
  2. 深入理解类型转换:特别是C++风格的显式转换,通过实际应用加深理解。
  3. 在算术运算中不要使用chat或者bool类型,因为chat有可能是signed chat,也可能是unsigned chat,具体由编译器来决定,而bool没有规定存储的大小。
  4. 执行浮点运算时选用double,这时因为float通常精度不够而且双精度浮点运算和单精度浮点运算的计算代价差不多,甚至在某些平台上可能双精度还更快。

2.2 变量

概述

本小节介绍C++中的变量,包括变量的定义、初始化、作用域和生命周期等。变量是程序中用于存储和操作数据的基本单元,理解变量的概念和使用方法是编写C++程序的基础。

2.2.1 变量定义

在C++中,变量的定义包括变量类型和变量名,可以在定义时进行初始化。

  • 基本语法
类型 变量名 = 初始值;

  • 示例
int i = 0;  // 定义一个整型变量并初始化为0
double d = 3.14;  // 定义一个双精度浮点型变量并初始化为3.14

变量初始化是为变量赋初值,可以在定义时进行。C++支持多种初始化方式,包括拷贝初始化、直接初始化和列表初始化。

拷贝初始化

拷贝初始化使用赋值运算符将初始值赋给变量。

  • 语法
int i = 0;
double d = 3.14;

直接初始化

直接初始化使用括号将初始值赋给变量。

  • 语法
int i(0);
double d(3.14);

列表初始化

列表初始化使用花括号将初始值赋给变量,C++11引入的这种方式可以防止窄化转换(narrowing conversion)。

  • 语法
int i{0};
double d{3.14};

2.2.2 变量声明和定义的关系

在C++中,声明和定义是两个不同的概念。理解变量的声明和定义之间的关系有助于编写更清晰和模块化的代码。

变量声明

变量声明告诉编译器变量的类型和名字,但不分配存储空间。声明通常出现在头文件中,以便在多个文件中共享变量。

  • 示例
extern int i;  // 声明一个整型变量i
extern double pi;  // 声明一个双精度浮点型变量pi
  • 注意extern关键字表示声明而非定义。声明告诉编译器变量已经定义在其他地方。

变量定义

变量定义不仅告诉编译器变量的类型和名字,还分配存储空间并可以初始化变量。定义通常出现在源文件中。

  • 示例
int i = 42;  // 定义一个整型变量i并初始化为42
double pi = 3.14159;  // 定义一个双精度浮点型变量pi并初始化为3.14159

声明和定义的区别
  • 声明
    • 告诉编译器变量的存在及其类型。
    • 不分配存储空间。
    • 使用extern关键字。
  • 定义
    • 告诉编译器变量的存在及其类型。
    • 分配存储空间。
    • 可以进行初始化。
  • 示例
// 声明
extern int x;  // 告诉编译器变量x存在且类型为int

// 定义
int x = 10;  // 告诉编译器变量x存在且类型为int,并分配存储空间和初始化

多文件程序中的声明和定义

在多文件程序中,变量的声明和定义通常在不同的文件中。通过声明和定义的分离,可以实现代码的模块化和重用。

  • 头文件(declarations.h)
extern int global_variable;  // 声明全局变量

  • 源文件1(file1.cpp)
#include "declarations.h"

int global_variable = 100;  // 定义全局变量

int main() {
    // 使用global_variable
    return 0;
}

  • 源文件2(file2.cpp)
#include "declarations.h"

void some_function() {
    // 使用global_variable
}

2.2.3 变量的作用域

变量的作用域是指变量在程序中可见并且可以被访问的区域。C++中的变量作用域分为全局作用域、局部作用域和块作用域。

全局作用域

在所有函数之外定义的变量具有全局作用域,可以在整个程序中访问。

  • 示例
int global_var = 100;  // 全局变量

int main() {
    std::cout << global_var << std::endl;  // 访问全局变量
    return 0;
}

局部作用域

在函数内部定义的变量具有局部作用域,只能在定义它的函数内部访问。

  • 示例
int main() {
    int local_var = 10;  // 局部变量
    std::cout << local_var << std::endl;  // 访问局部变量
    return 0;
}

块作用域

在复合语句(如if、for、while语句的花括号内)中定义的变量具有块作用域,只能在定义它的块内部访问。

  • 示例
int main() {
    if (true) {
        int block_var = 5;  // 块作用域变量
        std::cout << block_var << std::endl;  // 访问块作用域变量
    }
    // std::cout << block_var << std::endl;  // 错误,超出块作用域
    return 0;
}

2.2.4 变量的生命周期

变量的生命周期是指变量在内存中存在的时间段。变量的生命周期与它的作用域相关。

  • 全局变量:在程序开始时分配内存,程序结束时释放内存。
  • 局部变量:在函数调用时分配内存,函数返回时释放内存。
  • 块作用域变量:在块进入时分配内存,块退出时释放内存。

重点与难点分析

重点

  1. 变量的定义和初始化:掌握变量的定义和多种初始化方式,包括拷贝初始化、直接初始化和列表初始化。
  2. 理解声明和定义的区别:掌握声明和定义的不同之处,特别是在多文件程序中的作用。
  3. extern关键字:理解extern关键字的作用,用于声明变量而不定义。
  4. 变量的作用域:理解全局作用域、局部作用域和块作用域的概念和应用。

难点

  1. 列表初始化:理解列表初始化的优势,特别是防止窄化转换。
  2. 链接错误的排查:初学者容易在多文件程序中遇到链接错误,需要理解声明和定义的关系来解决。
  3. 模块化编程:通过声明和定义的分离,实现代码的模块化和重用。
  4. 变量的作用域和生命周期:初学者容易混淆不同作用域和生命周期的概念,需要通过实际编程练习加深理解。

练习题解析
  1. 练习2.7:定义一个局部变量和一个全局变量,并在程序中分别访问它们。
    • 示例代码:
#include <iostream>

int global_var = 100;  // 全局变量

int main() {
    int local_var = 10;  // 局部变量
    std::cout << "Global variable: " << global_var << std::endl;
    std::cout << "Local variable: " << local_var << std::endl;
    return 0;
}

  1. 练习2.8:定义一个块作用域变量,并尝试在块外部访问它,观察编译器报错。
    • 示例代码:
#include <iostream>

int main() {
    if (true) {
        int block_var = 5;  // 块作用域变量
        std::cout << "Block variable inside block: " << block_var << std::endl;
    }
    // std::cout << "Block variable outside block: " << block_var << std::endl;  // 错误,超出块作用域
    return 0;
}

  1. 练习2.9:定义一个const变量,并尝试修改它,观察编译器报错。
    • 示例代码:
#include <iostream>

int main() {
    const int max_size = 100;  // 常量变量
    // max_size = 200;  // 错误,常量不能被修改
    std::cout << "Constant variable: " << max_size << std::endl;
    return 0;
}

总结与提高

本节总结

  1. 学习了C++中的变量定义和初始化方法,包括拷贝初始化、直接初始化和列表初始化。
  2. 理解了变量的声明和定义的区别。
  3. 学习了extern关键字的作用,用于声明变量而不定义。
  4. 了解了在多文件程序中如何通过声明和定义分离来实现模块化编程。
  5. 理解了变量的作用域,包括全局作用域、局部作用域和块作用域。

提高建议

  1. 多练习变量的定义和初始化:通过编写各种变量定义和初始化的小程序,熟悉不同初始化方法的用法。
  2. 多练习声明和定义的使用:通过编写多文件程序,练习变量的声明和定义,熟悉extern关键字的用法。
  3. 理解链接错误的排查:通过故意制造和解决链接错误,深入理解声明和定义的关系。
  4. 模块化编程:通过声明和定义的分离,实现代码的模块化和重用,提高编程能力和代码质量。
  5. 深入理解作用域和生命周期:通过实际编程练习,理解不同作用域和生命周期的概念及其对变量的影响。

2.3 复合类型

概述

复合类型是基于基本类型构建的类型,包括引用和指针。复合类型的使用可以更灵活地操作数据和内存,掌握复合类型是编写C++程序的重要技能。

2.3.1 引用

引用是某个已存在变量的别名,通过引用可以访问或修改该变量的值。引用在定义时必须初始化,一旦绑定某个变量后不能再绑定其他变量。

定义引用
  • 语法
类型 &引用名 = 变量名;

  • 示例
int i = 42;
int &ref = i;  // 定义引用ref,绑定变量i

引用的使用
  • 修改引用所指向的变量
int i = 42;
int &ref = i;  // ref是i的引用
ref = 100;     // 修改ref即修改i
std::cout << i << std::endl;  // 输出100

  • 引用的特性
    • 引用在初始化时必须绑定变量,不能绑定字面值或常量。
    • 引用一旦绑定变量后,不能再绑定其他变量。

2.3.2 指针

指针是存储另一个变量地址的变量,通过指针可以间接访问或修改变量。指针在定义时可以不初始化,指向有效的内存地址后才能使用。

定义指针
  • 语法
类型 *指针名;

  • 示例
int i = 42;
int *p = &i;  // 定义指针p,指向变量i的地址

指针的使用
  • 访问指针所指向的变量
int i = 42;
int *p = &i;  // p是指向i的指针
*p = 100;     // 修改*p即修改i
std::cout << i << std::endl;  // 输出100

  • 指针的特性
    • 指针可以指向任何类型的变量,包括基本类型和复合类型。
    • 指针可以有多个级别,如指向指针的指针。

空指针和NULL指针
  • 空指针:空指针不指向任何对象,用于初始化指针或表示指针不指向有效地址。
int *p = nullptr;  // C++11引入的空指针表示法
int *p1 = 0;       // 传统的空指针表示法
int *p2 = NULL;    // 使用宏NULL定义的空指针

指针的运算

指针可以进行算术运算,如递增、递减和相减。

  • 指针递增递减
int arr[] = {1, 2, 3, 4, 5};
int *p = arr;  // 指向数组的第一个元素
++p;           // p指向数组的第二个元素
std::cout << *p << std::endl;  // 输出2

  • 指针相减
int arr[] = {1, 2, 3, 4, 5};
int *p1 = arr;
int *p2 = arr + 4;
std::cout << p2 - p1 << std::endl;  // 输出4,表示p2和p1之间相隔4个元素

2.3.3 const限定符与指针和引用

const限定符可以用来修饰指针和引用,使其所指向的对象不可修改。

指向常量的指针

指向常量的指针不能修改其指向的对象。

  • 定义指向常量的指针
const int *p = &i;  // p是指向常量的指针,不能通过p修改i的值

  • 示例
int i = 42;
const int *p = &i;
// *p = 100;  // 错误,不能通过p修改i的值
i = 100;    // 可以直接修改i的值
std::cout << *p << std::endl;  // 输出100

常量指针

常量指针本身是常量,不能修改其指向的地址。

  • 定义常量指针
int *const p = &i;  // p是常量指针,必须初始化,且不能修改指向

  • 示例
int i = 42;
int *const p = &i;  // 常量指针
*p = 100;           // 可以通过p修改i的值
// int j = 10;
// p = &j;  // 错误,不能修改常量指针p的指向

指向常量的常量指针

指向常量的常量指针既不能修改指向的对象,也不能修改指针本身的指向。

  • 定义指向常量的常量指针
const int *const p = &i;  // p是指向常量的常量指针

  • 示例
int i = 42;
const int *const p = &i;
// *p = 100;  // 错误,不能通过p修改i的值
// p = &j;    // 错误,不能修改常量指针p的指向

重点与难点分析

重点

  1. 引用的定义和使用:理解引用的概念及其用途,掌握引用的定义和使用方法。
  2. 指针的定义和使用:掌握指针的定义和基本操作,特别是指针的运算和空指针的使用。
  3. const限定符的应用:理解const限定符与指针和引用的组合使用方法,掌握其在保护数据中的作用。

难点

  1. 指针运算:初学者容易混淆指针的递增、递减和相减操作,需要通过练习掌握。
  2. const限定符的组合使用:理解不同组合的const限定符与指针和引用的作用及其实际应用。

练习题解析
  1. 练习2.12:定义一个引用和一个指针,分别修改它们所指向的变量的值。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    int &ref = i;  // 定义引用
    int *p = &i;   // 定义指针

    ref = 100;     // 修改引用所指向的变量
    std::cout << "i after ref: " << i << std::endl;

    *p = 200;      // 修改指针所指向的变量
    std::cout << "i after p: " << i << std::endl;

    return 0;
}

  1. 练习2.13:定义一个指向常量的指针和一个常量指针,尝试修改它们的指向和指向的值。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    const int *p1 = &i;  // 指向常量的指针
    int *const p2 = &i;  // 常量指针

    // *p1 = 100;  // 错误,不能通过p1修改i的值
    i = 100;     // 可以直接修改i的值
    std::cout << "i after p1: " << *p1 << std::endl;

    *p2 = 200;   // 可以通过p2修改i的值
    // int j = 300;
    // p2 = &j;  // 错误,不能修改常量指针p2的指向
    std::cout << "i after p2: " << *p2 << std::endl;

    return 0;
}

  1. 练习2.14:定义一个指向常量的常量指针,并尝试修改其指向和指向的值。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    const int *const p = &i;  // 指向常量的常量指针

    // *p = 100;  // 错误,不能通过p修改i的值
    // int j = 200;
    // p = &j;    // 错误,不能修改常量指针p的指向

    std::cout << "i: " << *p << std::endl;
    return 0;
}

总结与提高

本节总结

  1. 了解了引用的定义和使用方法,理解引用的基本概念。
  2. 学习了指针的定义和使用方法,掌握指针的基本操作和空指针的使用。
  3. 掌握了const限定符与指针和引用的组合使用方法,理解其在数据保护中的作用。

提高建议

  1. 多练习引用和指针的使用:通过编写各种引用和指针的小程序,熟悉它们的定义和操作。
  2. 深入理解指针运算:通过实际编程练习,掌握指针的递增、递减和相减操作。
  3. 应用const限定符:在程序中尽量使用const限定符保护数据,提高程序的安全性和可读性。

2.4 const限定符

概述

const关键字用于定义常量,它可以修饰变量、指针和函数,表示这些元素的值在程序运行期间不可改变。const限定符的合理使用可以提高程序的安全性和可读性。

2.4.1 定义常量

常量是值不能改变的变量,用const关键字修饰。

  • 基本语法
const 类型 变量名 = 初始值;

  • 示例
const int max_value = 100;  // 定义一个整型常量

常量初始化

常量在定义时必须进行初始化,因为之后不能修改其值。

  • 示例
const double pi = 3.14159;  // 定义并初始化常量pi

2.4.2 const引用

const引用是引用常量值的引用,不能通过const引用修改其绑定的对象。

  • 定义const引用
const 类型 &引用名 = 变量名;

  • 示例
int i = 42;
const int &ref = i;  // 定义一个const引用
// ref = 100;  // 错误,不能通过const引用修改i的值
i = 100;  // 可以直接修改i的值
std::cout << ref << std::endl;  // 输出100

const引用绑定字面值

const引用可以绑定到字面值、常量和不同类型的对象上。

  • 示例
const int &ref = 42;  // const引用绑定字面值
const double &ref2 = 3.14;  // const引用绑定浮点字面值

2.4.3 const指针

指针与const结合可以有不同的含义,取决于const在声明中的位置。

指向常量的指针

指向常量的指针不能通过该指针修改指向的对象。

  • 定义指向常量的指针
const 类型 *指针名;

  • 示例
int i = 42;
const int *p = &i;  // 定义指向常量的指针
// *p = 100;  // 错误,不能通过p修改i的值
i = 100;  // 可以直接修改i的值

常量指针

常量指针本身是常量,不能修改其指向的地址。

  • 定义常量指针
类型 *const 指针名;

  • 示例
int i = 42;
int *const p = &i;  // 定义常量指针
*p = 100;  // 可以通过p修改i的值
// int j = 10;
// p = &j;  // 错误,不能修改常量指针的指向

指向常量的常量指针

指向常量的常量指针既不能修改指向的对象,也不能修改指针本身的指向。

  • 定义指向常量的常量指针
const 类型 *const 指针名;

  • 示例
int i = 42;
const int *const p = &i;  // 定义指向常量的常量指针
// *p = 100;  // 错误,不能通过p修改i的值
// int j = 10;
// p = &j;  // 错误,不能修改指针p的指向

2.4.4 顶层const与底层const

const在C++中有顶层const和底层const的区别。

顶层const

顶层const表示指针本身是常量,或者是常量对象。

  • 示例
const int ci = 42;  // ci是顶层const
int *const p = &i;  // p是顶层const

底层const

底层const表示指针或引用所指向的对象是常量。

  • 示例
const int *p = &ci;  // p是底层const
const int &ref = ci;  // ref是底层const

2.4.5 constexpr和常量表达式

C++11引入了constexpr关键字,用于表示常量表达式。常量表达式是在编译时就能求值的表达式,使用constexpr可以提高程序的执行效率和安全性。

常量表达式

常量表达式是在编译时就能求值的表达式。常量表达式可以是字面值、常量、以及由常量表达式构成的表达式。

  • 示例
constexpr int size = 100;  // 常量表达式

constexpr和函数

constexpr可以用于函数声明,表示该函数是一个常量表达式函数,返回值可以在编译时求值。constexpr函数必须满足以下条件:

  • 函数体中只能包含单一的return语句或一系列能在编译时求值的表达式。
  • 所有参数和返回类型必须是字面值类型。
  • 示例
constexpr int square(int x) {
    return x * x;
}

constexpr int result = square(10);  // result在编译时计算

constexpr和const的区别
  • const:定义在运行时不变的常量,不能修改其值。
  • constexpr:定义在编译时求值的常量表达式,常用于编译时常量计算和优化。
  • 示例
const int const_var = 100;  // 运行时常量
constexpr int constexpr_var = 200;  // 编译时常量

重点与难点分析

重点

  1. 定义和使用常量:掌握const关键字的基本用法,理解常量的定义和初始化方法。
  2. const引用和指针:了解const引用和指针的定义及使用,理解其在保护数据中的作用。
  3. 顶层const与底层const:区分顶层const和底层const的概念及其应用。
  4. 理解constexpr的用法:掌握如何定义constexpr变量和constexpr函数,理解它们在编译时求值的特性。
  5. 使用constexpr提高性能:通过使用constexpr关键字,可以在编译时计算常量表达式,提高程序的执行效率。

难点

  1. const指针的组合使用:初学者容易混淆指向常量的指针、常量指针和指向常量的常量指针,需要通过练习掌握。
  2. 顶层const与底层const的区分:理解顶层const和底层const的区别及其在实际编程中的应用。
  3. constexpr函数的定义:确保constexpr函数的所有参数和返回类型都是字面值类型,并且函数体中只能包含能在编译时求值的表达式。
  4. constexprconst的区别:理解constexprconst的不同应用场景和各自的优缺点。

练习题解析
  1. 练习2.15:定义一个const引用和一个const指针,尝试修改它们所指向的变量的值。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    const int &ref = i;  // 定义const引用
    const int *p = &i;  // 定义指向常量的指针

    // ref = 100;  // 错误,不能通过const引用修改i的值
    // *p = 100;  // 错误,不能通过指向常量的指针修改i的值
    i = 100;  // 可以直接修改i的值

    std::cout << "ref: " << ref << std::endl;
    std::cout << "*p: " << *p << std::endl;
    return 0;
}

  1. 练习2.16:定义一个常量指针和一个指向常量的常量指针,尝试修改它们的指向和指向的值。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    int j = 10;
    int *const p1 = &i;  // 定义常量指针
    const int *const p2 = &i;  // 定义指向常量的常量指针

    *p1 = 100;  // 可以通过常量指针修改i的值
    // p1 = &j;  // 错误,不能修改常量指针的指向
    
    // *p2 = 200;  // 错误,不能通过指向常量的常量指针修改i的值
    // p2 = &j;  // 错误,不能修改指向常量的常量指针的指向

    std::cout << "i: " << i << std::endl;
    std::cout << "*p1: " << *p1 << std::endl;
    std::cout << "*p2: " << *p2 << std::endl;
    return 0;
}

  1. 练习2.17:定义一个顶层const变量和一个底层const指针,理解其用法。
    • 示例代码:
#include <iostream>

int main() {
    const int ci = 42;  // 顶层const
    const int *p = &ci;  // 底层const指针

    // ci = 100;  // 错误,不能修改顶层const变量的值
    // *p = 100;  // 错误,不能通过底层const指针修改ci的值
    
    std::cout << "ci: " << ci << std::endl;
    std::cout << "*p: " << *p << std::endl;
    return 0;
}

  1. 练习2.18:定义一个constexpr函数,计算两个整数的和,并在编译时求值。
    • 示例代码:
#include <iostream>

constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    constexpr int result = add(10, 20);  // result在编译时计算
    std::cout << "Result: " << result << std::endl;
    return 0;
}

  1. 练习2.19:比较constconstexpr的用法,定义运行时常量和编译时常量。
    • 示例代码:
#include <iostream>

const int run_time_const = 100;  // 运行时常量
constexpr int compile_time_const = 200;  // 编译时常量

int main() {
    std::cout << "Runtime const: " << run_time_const << std::endl;
    std::cout << "Compiletime const: " << compile_time_const << std::endl;
    return 0;
}

总结与提高

本节总结

  1. 学习了const关键字的用法,理解了常量的定义和初始化方法。
  2. 掌握了const引用和指针的定义及其在保护数据中的作用。
  3. 了解了顶层const和底层const的区别及其应用。
  4. 学习了constexpr关键字的用法,理解了常量表达式的概念及其在编译时求值的特性。
  5. 掌握了如何定义constexpr变量和constexpr函数,理解它们在提高程序性能中的作用。

提高建议

  1. 多练习const的使用:通过编写各种const引用和指针的小程序,熟悉它们的定义和操作。
  2. 深入理解顶层const与底层const:通过实际编程练习,掌握顶层const和底层const的区别及其在实际应用中的作用。
  3. 应用const保护数据:在程序中尽量使用const关键字保护数据,提高程序的安全性和可读性。
  4. 多练习constexpr的使用:通过编写各种constexpr变量和函数的小程序,熟悉它们的定义和操作。
  5. 理解编译时常量和运行时常量的区别:通过实际编程练习,掌握constexprconst的不同应用场景及其优缺点。
  6. 优化程序性能:在实际编程中,尽量使用constexpr关键字进行编译时计算和优化,提高程序的执行效率和安全性。

2.5 处理类型

概述

本小节介绍C++中的类型处理方法,包括类型别名、autodecltype关键字。通过这些工具,可以简化代码,提高可读性,并在编译时确定变量类型。

2.5.1 类型别名

类型别名用于为现有类型创建新的名称,可以使用typedefusing关键字。

使用typedef定义类型别名

typedef关键字用于定义类型别名,使代码更易读和维护。

  • 语法
typedef 原类型 新类型;

  • 示例
typedef double wages;  // 定义wages为double的别名
typedef wages base, *p;  // base是double的别名,p是指向double的指针类型

使用using定义类型别名

C++11引入了using关键字,可以替代typedef,更直观易读。

  • 语法
using 新类型 = 原类型;

  • 示例
using wages = double;  // 定义wages为double的别名
using base = wages;    // base是wages的别名,即double的别名
using p = base*;       // p是指向base的指针类型

2.5.2 auto类型说明符

auto关键字用于自动推导变量的类型,编译器根据初始值的类型推断变量的类型。

使用auto定义变量
  • 语法
auto 变量名 = 初始值;

  • 示例
auto i = 42;  // i被推断为int类型
auto d = 3.14;  // d被推断为double类型

使用auto简化代码

auto可以简化复杂类型的定义,特别是在使用模板和迭代器时。

  • 示例
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}

2.5.3 decltype类型指示符

decltype关键字用于查询表达式的类型,编译器分析表达式并推断其类型。

使用decltype获取类型
  • 语法
decltype(表达式) 变量名 = 初始值;

  • 示例
int i = 42;
decltype(i) j = i;  // j的类型被推断为int

使用decltype获取函数返回类型

decltype可以用来声明函数的返回类型,特别是在返回类型依赖于参数类型时。

  • 示例
template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
    return a + b;
}

重点与难点分析

重点

  1. 类型别名的定义和使用:掌握typedefusing的用法,理解它们在代码简化和可读性提高中的作用。
  2. auto关键字:学习如何使用auto自动推导变量类型,简化代码。
  3. decltype关键字:理解decltype的用法,掌握如何获取表达式的类型。

难点

  1. autodecltype的实际应用:初学者需要通过实际编程练习,理解和掌握autodecltype的使用场景和技巧。

练习题解析
  1. 练习2.21:使用typedefusing分别定义类型别名,并使用这些别名定义变量。
    • 示例代码:
#include <iostream>

typedef double wages_t;  // 使用typedef定义类型别名
using salary_t = double;  // 使用using定义类型别名

int main() {
    wages_t hourly_wage = 20.5;
    salary_t annual_salary = 50000.0;

    std::cout << "Hourly Wage: " << hourly_wage << std::endl;
    std::cout << "Annual Salary: " << annual_salary << std::endl;
    return 0;
}

  1. 练习2.22:编写一个使用auto关键字推导变量类型的示例程序。
    • 示例代码:
#include <iostream>
#include <vector>

int main() {
    auto i = 42;  // 自动推导类型为int
    auto d = 3.14;  // 自动推导类型为double
    std::vector<int> vec = {1, 2, 3, 4, 5};

    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

  1. 练习2.23:编写一个使用decltype关键字推导表达式类型的示例程序。
    • 示例代码:
#include <iostream>

int main() {
    int i = 42;
    decltype(i) j = i;  // j的类型被推导为int

    auto add = [](decltype(i) a, decltype(i) b) -> decltype(a + b) {
        return a + b;
    };

    std::cout << "j: " << j << std::endl;
    std::cout << "add(i, j): " << add(i, j) << std::endl;

    return 0;
}

总结与提高

本节总结

  1. 了解了类型别名的定义和使用方法,掌握了typedefusing关键字。
  2. 学习了auto关键字的用法,理解了自动推导变量类型的优势。
  3. 掌握了decltype关键字的用法,能够获取表达式的类型。

提高建议

  1. 多练习类型别名的使用:通过编写各种类型别名的小程序,熟悉typedefusing的定义和应用。
  2. 深入理解autodecltype:通过实际编程练习,掌握autodecltype在不同场景中的应用和优

2.6 自定义数据结构

概述

本小节介绍如何在C++中定义和使用自定义数据结构。自定义数据结构可以帮助我们更有效地组织和管理复杂的数据。C++中常用的自定义数据结构包括结构体(struct)和联合体(union)。

2.6.1 结构体(struct)

结构体是一种将不同类型的数据组合在一起的复合类型。结构体成员可以是基本类型、数组、指针、甚至是其他结构体。

定义和使用结构体
  • 定义结构体
struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

  • 使用结构体
int main() {
    Sales_data book1;
    book1.bookNo = "978-0590353403";
    book1.units_sold = 25;
    book1.revenue = book1.units_sold * 20.5;

    std::cout << "Book No: " << book1.bookNo << std::endl;
    std::cout << "Units Sold: " << book1.units_sold << std::endl;
    std::cout << "Revenue: " << book1.revenue << std::endl;

    return 0;
}

结构体的初始化

可以在定义结构体变量时进行初始化。

  • 列表初始化
Sales_data book2 = {"978-0590353403", 30, 615.0};

  • 默认初始化
Sales_data book3;  // book3的成员会被默认初始化

结构体的成员访问

结构体的成员通过点操作符(.)访问和修改。

  • 访问和修改成员
book1.units_sold = 50;
double avg_price = book1.revenue / book1.units_sold;

2.6.2 联合体(union)

联合体是一种节省空间的类类型,它允许其成员共享同一块内存。联合体一次只能存储一个成员的值。

定义和使用联合体
  • 定义联合体
union Token {
    char cval;
    int ival;
    double dval;
};

  • 使用联合体
int main() {
    Token t;
    t.cval = 'c';
    std::cout << "Token as char: " << t.cval << std::endl;

    t.ival = 42;
    std::cout << "Token as int: " << t.ival << std::endl;

    t.dval = 3.14;
    std::cout << "Token as double: " << t.dval << std::endl;

    return 0;
}

联合体的成员访问

联合体的成员通过点操作符访问和修改。需要注意的是,联合体的不同成员共享同一块内存,因此同时访问多个成员是不安全的。

  • 访问和修改成员
t.ival = 10;
t.dval = 20.5;  // t.ival的值可能会被覆盖

2.6.3 枚举类型

枚举类型是用户定义的一种类型,它由一组命名的整型常量组成。枚举类型提高了代码的可读性和可维护性。

定义和使用枚举类型
  • 定义枚举类型
enum class Color { Red, Green, Blue };

  • 使用枚举类型
int main() {
    Color color = Color::Red;

    switch (color) {
    case Color::Red:
        std::cout << "Red" << std::endl;
        break;
    case Color::Green:
        std::cout << "Green" << std::endl;
        break;
    case Color::Blue:
        std::cout << "Blue" << std::endl;
        break;
    }

    return 0;
}

枚举类型的作用域

C++11引入了强作用域枚举(enum class),它们的枚举值不会被提升到外层作用域。

  • 示例
enum class Color { Red, Green, Blue };
Color c = Color::Red;
// int n = Color::Red;  // 错误,Color::Red没有提升到外层作用域

重点与难点分析

重点

  1. 结构体的定义和使用:掌握结构体的基本概念和用法,包括定义、初始化和成员访问。
  2. 联合体的定义和使用:了解联合体的特点和用法,理解其节省空间的优势。
  3. 枚举类型的定义和使用:掌握枚举类型的基本概念和用法,特别是强作用域枚举(enum class)。

难点

  1. 联合体的内存共享:理解联合体成员共享同一块内存的特性,并注意避免同时访问多个成员。
  2. 强作用域枚举的使用:掌握enum class的定义和使用,理解其作用域规则。
练习题解析
  1. 练习2.24:定义一个结构体表示书籍信息,并编写程序输出书籍的详细信息。
    • 示例代码:
#include <iostream>
#include <string>

struct Book {
    std::string title;
    std::string author;
    double price;
};

int main() {
    Book book = {"The C++ Programming Language", "Bjarne Stroustrup", 59.99};

    std::cout << "Title: " << book.title << std::endl;
    std::cout << "Author: " << book.author << std::endl;
    std::cout << "Price: $" << book.price << std::endl;

    return 0;
}

  1. 练习2.25:定义一个联合体存储不同类型的数据,并编写程序分别输出这些数据。
    • 示例代码:
#include <iostream>

union Data {
    int ival;
    float fval;
    char cval;
};

int main() {
    Data data;
    data.ival = 42;
    std::cout << "Data as int: " << data.ival << std::endl;

    data.fval = 3.14f;
    std::cout << "Data as float: " << data.fval << std::endl;

    data.cval = 'c';
    std::cout << "Data as char: " << data.cval << std::endl;

    return 0;
}

  1. 练习2.26:定义一个枚举类型表示颜色,并编写程序输出对应的颜色名称。
    • 示例代码:
#include <iostream>

enum class Color { Red, Green, Blue };

int main() {
    Color color = Color::Green;

    switch (color) {
    case Color::Red:
        std::cout << "Red" << std::endl;
        break;
    case Color::Green:
        std::cout << "Green" << std::endl;
        break;
    case Color::Blue:
        std::cout << "Blue" << std::endl;
        break;
    }

    return 0;
}

总结与提高

本节总结

  1. 学习了结构体的定义和使用方法,理解了结构体在组织和管理复杂数据中的作用。
  2. 掌握了联合体的特点和用法,理解了其节省空间的优势。
  3. 了解了枚举类型的定义和使用方法,特别是强作用域枚举(enum class)的优势。

提高建议

  1. 多练习结构体和联合体的使用:通过编写各种结构体和联合体的小程序,熟悉它们的定义和操作。
  2. 深入理解联合体的内存共享特性:通过实际编程练习,掌握联合体成员共享内存的特性,注意避免同时访问多个成员。
  3. 应用枚举类型提高代码可读性:在程序中使用枚举类型表示有限的状态或选项,提高代码的可读性和可维护性。

本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
### 回答1: 《C Primer》(第5版)是一本C语言的入门教材,对于初学者来说是一本很好的学习资料。它以中文回答了大量的C语言习题,为读者提供了一个循序渐进的学习过程。 这本书有一些优点,首先是适合各种学习条件的读者。无论是想要在大学学习C语言的学生,还是自学编程的初学者,都可以从这本书中受益。其次,书中的内容易于理解。作者采用简明易懂的语言,讲解了C语言的基础知识和编程技巧,帮助读者快速掌握C语言。第三,书中的习题丰富多样,涵盖了从简单到复杂的各个方面。这些习题不仅有助于加深对C语言知识的理解,还可以帮助读者锻炼编程能力。 由于本人无法提供PDF文档,请读者自行查询并下载该书的PDF版本。这本书不仅仅是一个习题集,它还包含了大量的示例代码和案例分析,这些都对读者的学习和实践提供了很好的帮助。 总之,《C Primer》(第5版)是一本很棒的C语言学习资料,读者可以通过习题的练习提高自己的编程能力,掌握C语言的基础知识。希望读者能够借助这本书的帮助,快速地学会C语言编程。 ### 回答2: " C Primer习题集:第5版pdf " 是一本同名教材的习题集,针对C语言编程进行了深入的练习。该习题集的PDF版本可以在网络上进行下载。 这本习题集是为了帮助C语言初学者提高他们的编程技能。它从基础知识开始,逐渐深入到更高级的概念和技术。习题集的节组织结构与原版教材一致,旨在提供一个系统和综合的学习体验。 这本习题集涵盖了许多重要的编程主题,如变量、运算符、控制流程、数组、字符串、指针、结构体和文件。每个节都包含大量的习题,涵盖了不同难度级别的问题。这些习题可帮助学生巩固已学知识,并提供了实践机会以应用所学的概念。 与原版教材相比,该习题集以问题为驱动,需要学生进行自我思考和解答。通过反复实践和解决问题,学生可以更好地理解C语言的概念和技术。 总的来说, C Primer习题集:第5版pdf 提供了一个实践性强、系统性好的学习工具,适合想要提高他们C语言编程技能的人使用。通过完成这些习题,学生可以更深入地理解C语言的各个方面,并在实际项目中应用所学的知识。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iShare_爱分享

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值