1. C++变量和基本类型(二)
声明:以C++ Primer 第二章习题为基础学习C++变量和基本类型
1.1 练习2.15 引用的含义
(a) int ival = 1.01;
定义整型却初始化为浮点型 (做错了,应该是合法的)
(b) int& val1 = 1.01;
引用错误
(c) int& rval2 = ival;
正确引用,但ival是错误定义的 (做错了,应该是合法的)
(d) int& rval3;
未明确引用
更正:
(a) 是合法的,因为浮点数 1.01 可以自动截断为整型 1。
(b) 是非法的,因为 int& 不能绑定到 double 常量 1.01。
(c) 是合法的,因为引用 rval2 被正确地绑定到同类型的变量 ival。
(d) 是非法的,因为引用 rval3 没有初始化,而引用必须在定义时初始化。
基础知识补充:
在C++中,引用(Reference)是一种数据类型,它是一个已存在变量的别名,并且在语法上提供了类似指针的功能,但使用更加简洁直观。引用的引入让代码更简洁,同时能够更有效地操作对象和数据。
当你创建一个引用时,它相当于为已有变量创建了一个新的名称。这个新的名称和原变量共享同一块内存地址,对引用的任何操作实际上就是对原变量的操作。
引用的特点:必须初始化、不能更改绑定对象
必须初始化:引用在定义时必须绑定到一个已有的变量或对象,不能像指针那样声明之后再赋值
不可重新绑定:一旦引用绑定到某个变量,就无法更改引用的对象。这一点与指针不同,指针可以重新指向不同的对象,而引用一经绑定无法更改。
int a = 10;
int &ref; // 错误,引用必须初始化
int &ref = a; // 正确,ref 是 a 的引用
引用用于提高效率
在函数参数传递中,引用特别有用。传值参数会拷贝实参的值,而传引用参数则不会拷贝数据,而是直接操作原变量,尤其在处理大型对象时,引用可以显著提升性能。
void update(int &num) {
num = 20; // 修改的是调用时传入的原变量
}
int main() {
int a = 10;
update(a); // a 被修改为 20
}
const 引用用于保护数据
const 引用允许我们创建只读的引用,这样可以防止引用修改原始数据。const 引用还允许我们绑定临时对象和字面值:
void display(const int &num) {
std::cout << num << std::endl;
}
int main() {
int a = 10;
display(a); // 传递左值,lvalue,location value,具有稳定存储位置的对象,程序在其作用域内始终可以通过内存地址访问它。通常左值是可修改的,但若被声明为 const,则不允许修改。
display(100); // 传递右值,rvalue,read value,右值通常是表达式计算的临时结果,无法通过地址来引用,右值通常不具备可修改性,特别是在表达式中,编译器会在表达式结束后销毁它。
}
左值(lvalue,location value)是指在程序中有明确存储位置的对象,它们通常是具名变量,或者能被引用的对象。左值可以出现在赋值语句的左侧,也可以出现在右侧。
- 有持久的存储位置:左值是具有稳定存储位置的对象,程序在其作用域内始终可以通过内存地址访问它。
- 可修改:通常左值是可修改的,但若被声明为 const,则不允许修改
int x = 10; // x 是左值,因为它有明确的存储位置
x = 20; // 可以修改 x 的值
int &ref = x; // 左值引用可以绑定到左值 x
右值(rvalue,read value)是指没有明确存储位置的临时对象,通常是字面量、表达式计算的结果,或需要被销毁的临时对象。右值一般只能出现在赋值语句的右侧,不能被赋值。
- 临时对象:右值通常是表达式计算的临时结果,无法通过地址来引用。
- 不可修改:右值通常不具备可修改性,特别是在表达式中,编译器会在表达式结束后销毁它。
int x = 10;
int y = x + 5; // x + 5 是右值,表达式结果为临时值
int z = 20; // 20 是右值常量
左值和右值的实际用途
左值用于持久性对象:在程序中需要长期使用、反复访问的数据应该作为左值。例如,变量和对象都是左值。
右值用于临时数据:如果一个数据只在短期内使用一次或立即处理完就可以销毁,适合使用右值。字面量、表达式结果通常就是右值。
引用的类型
非常量引用
#include <iostream>
int main() {
int i = 5;
int& ri = i;
std::cout << i << " " << ri << '\n';
i = 8; //通过原对象修改值
std::cout << i << " " << ri << '\n';
ri = 3; //通过引用修改值
std::cout << i << " " << ri;
return 0;
}
==============
5 5
8 8
3 3
常量引用
#include <iostream>
int main() {
int i = 5;
int const& ri = i; //常量引用
std::cout << i << " " << ri << '\n';
i = 8; //通过原对象修改值
std::cout << i << " " << ri;
//ri = 3; //error: assignment of read-only reference ‘ri’
//std::cout << i << " " << ri << '\n';
return 0;
}
===============
5 5
8 8
auto引用
#include <iostream>
int main() {
int i = 5;
double d = 3.14;
double x = i + d;
auto & ri = i; //自动类型推导ri:int&
auto const& crx = x; //自动类型推导crx:double const&
std::cout << i << " " << ri << '\n';
i = 8; //通过原对象修改值
std::cout << i << " " << ri;
//crx = 3; //error: assignment of read-only reference ‘crx’
//std::cout << x << " " << crx << '\n';
return 0;
}
================
5 5
8 8
左值引用与右值引用的对比
引用的应用场景及示例
引用与指针的对比
1.2 练习2.16 引用的用法
int i = 0, &r1 = i;
double d = 0, &r2 = d;
(a) r2 = 3.14159
合法,对r2和d进行了修改
(b) r2 = r1;
不合法,r2已经绑定了d,绑定后不可改变(做错了,应该是合法了,这里没有重新绑定对象,而是直接赋值)
(c) i = r2;
合法,将r2的值赋值给i
(d) r1 = d;
合法,将d的值赋值给r1
1.3 练习2.17 引用的含义及用法
#include <iostream>
int main() {
int i, &ri = i;
i = 5;
std::cout << i << " " << ri << std::endl;
ri = 10;
std::cout << i << " " << ri << std::endl;
return 0;
}
==========
5 5
10 10
1.4 练习2.18 指针的含义
#include <iostream>
int main() {
int a = 10;
int* p = &a;
std::cout << p << " " << *p << '\n';
//修改指针的值
p = p + 1; //错误,将指针指向了一个没有被初始化值的地方
std::cout << p << " " << *p << '\n';
//修改指针所指对象的值
*p = 20; //错误
std::cout << p << " " << *p << '\n';
return 0;
}
=============
0x7fffffffdedc 10
0x7fffffffdee0 -8480 //错误
更正:
#include <iostream>
int main() {
int i = 5, j = 10;
int* p = &i;
std::cout << p << " " << *p << '\n';
//修改指针的值,指针从i指向j
p = &j; //获取j的存储地址并由p保存
std::cout << p << " " << *p << '\n';
//修改指针所指对象的值
*p = 20; //p指向了j,更改p所指对象的值为20,即j=20
std::cout << p << " " << *p << '\n';
j = 30; //j变为了30
std::cout << p << " " << *p << '\n';
return 0;
}
===============
0x7ffcd06f2328 5
0x7ffcd06f232c 10
0x7ffcd06f232c 20
0x7ffcd06f232c 30
基础知识补充:
为什么需要指针?
下图来自:Pointers
1.5 练习2.19 指针和引用的区别
引用与指针的对比
1.6 练习2.20 运算符*
#include <iostream>
int main() {
int i = 42;
int* p1 = &i; //指针声明符
*p1 = *p1 * *p1; //42*42, 解引用运算符
std::cout << *p1;
return 0;
}
========
1764
1.7 练习2.21 指针的声明和定义
(a) double* dp = &i;
i为整型,而dp为double类型指针
(b) int* ip = i;
没有获得i的地址
(c) int* p = &i;
合法
知识拓展:
野指针:指向未知地址的指针。未初始化的指针可能成为野指针,容易导致程序崩溃。
int *p; // p 未初始化,是一个野指针
*p = 10; // 未定义行为
悬空指针:指向已释放或超出作用域的内存的指针,可能导致未定义行为
int *p = new int(10);
delete p; // 释放内存
*p = 20; // 未定义行为,p 是悬空指针
1.8 练习2.22 指针的值和指针所指对象的值
if(p)//检查指针是否为空
if(*p)//检查指针所指对象是否为空
#include <iostream>
int main() {
int i = 0;
int* p1 = nullptr;
int* p = &i;
if (p1) //检查p1是否为空指针
std::cout << "p1 pass" << std::endl;
if (p) //检查p是否为空指针
std::cout << "p pass" << std::endl;
if (*p)//检查p所指对象是否为0
std::cout << "i pass" << std::endl;
return 0;
}
==========
p pass
1.9 练习2.23 指针初始化
int* p = nullptr;
if(p)//检查是否为空指针
//若通过条件检查,则指向了合法对象
1.10 练习2.24 void* 指针
int i = 42;
void* p = &i;
(不清楚为什么合法)
long* lp = &i;
i为整型变量,lp为长整型指针,不匹配
1.11 练习2.25 指针和引用声明
(a) int* ip, i, &r = i;
ip整型指针,i整型变量,r引用
(b) int i, *ip = 0;
整型变量i,空指针ip
(c) int* ip, ip2;
未初始化整型指针ip,整型变量ip2
1.12 练习2.26 const限定符
(a) const int buf;
未初始化
(b) int cnt = 0;
合法
(c) const int sz = cnt;
合法
(d) ++cnt; ++sz;
cnt允许修改,sz不允许修改
基础知识补充:
下面内容来源:CPlusPlusThings/basic_content/const/
常量防止修改,起保护作用
const int a=100; //常量在定义后就不能被修改,所以定义时必须初始化
const对象默认为文件局部变量
未被const修饰的变量在不同文件的访问
//CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(practice)
set(CMAKE_CXX_STANDARD 17)
add_executable(practice main.cpp file2.cpp)
// file2.cpp
int ext;
// main.cpp
#include<iostream>
extern int ext;
int main(){
std::cout<<(ext+10)<<std::endl;
}
const常量在不同文件的访问
const对象默认为文件局部变量,要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern
//CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(practice)
set(CMAKE_CXX_STANDARD 17)
add_executable(practice main.cpp file2.cpp)
// file2.cpp
//const int ext=2; 默认文件内的局部变量,若要在其他文件访问,显式指定extern
//常量在定义后就不能被修改,所以定义时必须初始化
extern const int ext=2;
//main.cpp
#include <iostream>
extern const int ext;
int main() {
std::cout<< ext << std::endl;
return 0;
}
指针与const
从右往左读,*与右侧结合
指向常量的指针
const char *p; //*p:p是一个指针,const char为指针所指对象的类型,该对象为char类型且其值不可更改
char const *p; //同上
常指针
char *const p; //*const p:p是一个不可更改指向的指针,指针p指向了char类型对象,该对象的值可以被更改
指向常量的常指针
const char *const p; //*const p:p是一个不可更改指向的指针,指针p指向了char类型对象,该对象的值不可更改
函数中使用const
(1)const修饰函数返回值
const int func1(); //本来就是范围int值,使用const与不使用没有区别
const int* func2(); //返回指针*,指针指向的类型为const int,即指针指向的内容不变
int *const func3(); //返回*const指针,指向指向的类型为int,即指针本身指向不能改变
(2)const修饰函数参数
传递来的整数和指针在函数体内不可改变,无意义
如果函数需要传入一个指针,是否需要为该指针加上const,把const加在指针不同的位置有什么区别;
void func(const int var); //传递来的整数var在函数体内不能被改变,那要这个函数干嘛?没有意义
void func(int *const var); //传递来的指针const var的指向在函数体内不能被改变,那要这个函数干嘛?没有意义
参数指针所指内容为常量不可改变
void StringCopy(char *dst, const char *src); //src是输入参数,dst是输出参数
//若函数体内试图改动src所指向的内容是不允许的
如果写的函数需要传入的参数是一个复杂类型的实例,传入值参数或者引用参数有什么区别,什么时候需要为传入的引用参数加上const?
参数为引用,为增加效率同时防止修改
按值传递:如果函数需要对传入的对象进行拷贝并且修改,或者对象比较小且拷贝成本不高,使用按值传递
按引用传递:如果函数不需要修改传入对象,且对象较大或拷贝成本较高,使用按引用传递(const A&)通常是更好的选择
//a 是按值传递的,会在调用函数时创建一个 A 类型的临时对象,这个临时对象会在函数执行完后被销毁
//按值传递会导致对象的拷贝,这可能带来额外的性能开销,
//其是在对象比较大的时候(例如包含大量成员变量或者动态分配内存的类)。
//但是,按值传递的优点是可以对传入的对象进行独立的修改,而不影响原始对象。
void func(A a) //函数体将产生A类型的临时对象用于复制参数a,临时对象构造、复制、析构将消耗时间
//按引用传递(常量引用)
//a 作为引用传递,它直接引用传入的对象,而不需要创建临时对象
//按引用传递(const A& a) 直接引用传入的对象,不会发生拷贝操作,性能更优,且通过 const 保证了对象不可修改
void func(const A& a) //const 自定义类型 &对象
//内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
void func(const int &x) 效率上与 void func(int x) 相当
类中使用const
class Apple {
private:
int people[100]; // 私有成员,数组 people 包含 100 个整数
public:
Apple(int i); // 构造函数声明,接受一个整数参数
const int apple_number; // 常量整型成员变量,表示苹果的数量
};
// 构造函数定义
//常量成员变量不能在构造函数的主体中赋值,只能在构造函数的初始化列表中进行赋值
//类::类成员函数:类成员常量
//Apple::Apple(int i) 这里是 Apple 类的构造函数的定义。构造函数的作用是当创建 Apple 对象时,初始化对象的状态
//: apple_number(i) 是 初始化列表(initializer list)的部分,表示通过构造函数参数 i 来初始化类中的常量成员 apple_number
Apple::Apple(int i) : apple_number(i) { // 初始化 apple_number 为传入的整数 i
// 构造函数体为空
}
例子:
CMakeLists.txt
// 该指令设置了 CMake 的最低版本要求。也就是说,只有 CMake 版本等于或大于 3.28 才能成功配置和构建此项目
cmake_minimum_required(VERSION 3.28)
// 该指令定义了项目的名称。这里的 practice 是项目的名称
project(practice)
// 设置项目的 C++ 标准版本为 C++17,它告诉 CMake 在编译过程中使用 C++17 标准。这相当于在编译器中传递 -std=c++17 参数
set(CMAKE_CXX_STANDARD 17)
// 该指令用于指定要创建的可执行文件及其源文件
// practice:这是生成的可执行文件的名称。执行 cmake 和 make 后,最终会生成一个名为 practice 的可执行文件
// main.cpp 和 apple.cpp这些是该项目的源代码文件,CMake 会将它们编译并链接在一起,生成最终的可执行文件
// CMake 将会将 main.cpp 和 apple.cpp 这两个源文件编译为对象文件,并链接它们,最终生成一个名为 practice 的可执行程序
add_executable(practice main.cpp apple.cpp) # 用于指定要创建的可执行文件及其源文件
apple.h
在 main.cpp 中使用 #include “apple.h”,CMake 会自动查找与 main.cpp 同一目录下的 apple.h 头文件,不需要额外的target_include_directories 配置
//用来避免头文件被重复包含
//当你在多个源文件中使用 #include 指令来包含同一个头文件时,可能会出现头文件被 多次包含 的情况
//#include "apple.h"
//#include "apple.h" // 如果没有保护,会导致重复包含
//如果没有保护措施,编译器会在第二次包含时再次读取并处理 apple.h 文件的内容,这会导致重复定义错误
#ifndef APPLE_H
#define APPLE_H
class Apple {
private:
//private,外部无法直接访问,只有类内的成员函数可以访问和修改它。
int people[100]; // 私有成员:一个包含 100 个整数的数组,用来存储数据
public:
//构造函数
Apple(int i); // 构造函数声明,接受一个整数参数 i
//成员变量
const int apple_number; // 公共成员:常量整型成员变量
//成员函数
void take(int num) const; // 成员函数:接受一个整数参数,标记为 const,修饰成员函数,表示函数内部不会修改类的成员变量
int add(); // 成员函数:接受一个整数参数,无 const
int add(int num) const; // 成员函数:接受一个整数参数,标记为 const,修饰成员函数,表示函数内部不会修改类的成员变量
int getCount() const; // 成员函数:无参数,标记为 const,修饰成员函数,表示函数内部不会修改类的成员变量
};
#endif //APPLE_H
apple.cpp
#include<iostream>
#include "apple.h"
//由于 apple_number 是 const,它只能在构造函数的初始化列表中初始化,而不能在构造函数体内赋值
Apple::Apple(int i) : apple_number(i){
}
//初始化const常量用初始化列表方式外,也可以通过其他方法:
//(1)将常量定义与static结合:static const int apple_number
//(2)在外面初始化:const int Apple::apple_number=10;
//(3)如果你使用c++11进行编译,直接可以在定义出初始化:
//static const int apple_number=10;
// 或者const int apple_number=10;
int Apple::add(int num) const{ //标记为 const 修饰成员函数,表示函数内部不会修改类的成员变量
take(num);
return 0;
}
void Apple::take(int num) const { //标记为 const 修饰成员函数,表示函数内部不会修改类的成员变量
std::cout << "take func" << num << std::endl;
}
int Apple::getCount() const { //标记为 const 修饰成员函数,表示函数内部不会修改类的成员变量
take(1);
//add(); //add方法并非const修饰,所以运行报错。也就是说const成员函数只能访问const成员函数
return apple_number;
}
main.cpp
#include <iostream>
#include "apple.h"
int main() {
//Apple对象a包含people数组,apple_number
//构造函数本身是类的成员函数,因此它能够访问类的所有成员(包括私有成员)。构造函数在被调用时,可以直接对私有成员进行初始化或赋值
Apple a(2); //调用构造函数并初始化常量成员变量apple_number
std::cout << a.getCount() << std::endl; //调用getCount函数(return apple_number) -> take(1) const函数 -> 打印take func num 1
a.add(10); //add(10) -> take(10) const函数 -> 打印take func 10
const Apple b(3);
b.add(100); //add(100) -> take(100) const函数-> 打印take func 100
return 0;
}
补充:初始化const常量的方法
一、通过构造函数的初始化列表初始化
//apple.h
#ifndef APPLE_H
#define APPLE_H
class Apple {
private:
...
public:
//构造函数
Apple(int i); // 构造函数声明,接受一个整数参数 i
//成员变量
const int apple_number;
//apple.cpp
Apple::Apple(int i) : apple_number(i){
}
二、通过类外部初始化静态常量成员
//apple.h
class Apple {
public:
static const int apple_number; //这是类内部的成员变量声明,表示 apple_number 是一个静态常量成员变量。它在类的所有对象之间共享,并且一旦初始化后不能修改
//它告诉编译器该变量是属于 Apple 类的,但是它的存储空间在哪里,具体值是多少,并没有提供任何信息
};
// 类外初始化 在apple.cpp中对其初始化
//静态成员变量的存储空间是在类的外部分配的,因为它是所有类的实例共享的
const int Apple::apple_number = 10;//这是类外初始化,即在类外部给 apple_number 赋初值。虽然 apple_number 已经在类内部声明为常量,C++ 允许你在类外为它初始化一个值(通常这是对静态常量成员变量的初始化)。这就是“外部初始化”指的内容
静态成员变量(即所有对象共享的常量)我们需要在 类外部 为这个静态成员变量提供初始化值。
为什么要类外初始化?
类外初始化是必要的,因为静态成员变量是属于类本身的,而不是某个具体的对象。虽然静态成员可以在类内部声明,但它们的存储空间是在类外部分配的,因此必须在类外部提供一个明确的初始化值。
存储分配:静态成员变量的内存是在程序的全局/静态存储区分配的,而不是每个对象的内存中。因此,它们的定义与类的实例化是分开的。
类的声明与定义分开:C++ 将类的声明和定义分开。在类声明(在apple.h内)中,只能声明成员变量,但无法为它们分配存储空间。因为类的定义 (在apple.cpp内) 可能会出现在多个源文件中(如 .h 文件会被多个 .cpp 文件包含),而类外初始化保证了每个源文件中都能正确地共享和初始化静态成员变量。
完整例子:
//apple.h
#ifndef APPLE_H
#define APPLE_H
class Apple {
public:
static int apple_number; // 声明静态成员变量
Apple(); // 构造函数
};
#endif // APPLE_H
//apple.cpp
#include "apple.h"
// 类外初始化静态成员变量
int Apple::apple_number = 10;
Apple::Apple() {
// 构造函数内容
}
//main.cpp
#include <iostream>
#include "apple.h"
int main() {
std::cout << "Apple number: " << Apple::apple_number << std::endl;
return 0;
}
三、C++11 新特性:直接在类内部初始化
这个方法只适用于 static const 类型的常量成员变量。非静态的常量成员变量必须通过构造函数的初始化列表来初始化
//apple.h
class Apple {
public:
static const int apple_number = 10; // 在类内部直接初始化
};
static 成员变量的初始化
static 成员变量是类的静态成员,属于类本身,而不是某个具体对象。它们在类的所有实例之间共享,且只能在类外部进行初始化,并且不能在类内初始化。
普通 static 成员变量
class Apple {
public:
//普通 static 成员变量
static int apple_number; // 这是声明
};
// 类外部进行初始化 在apple.cpp中
int Apple::apple_number = 10;
为什么普通 static 成员变量不能在类内初始化?
存储位置:static 成员变量的存储空间是在类外分配的(通常在程序的全局/静态存储区),所以它不能在类定义体内分配内存或初始化。
链接规则:类内定义静态成员变量的值会导致多个源文件出现重复定义,违反 C++ 的链接规则。而类外初始化可以确保该变量只有一个定义。
(1)static 成员变量是属于整个类的,而不是某个具体的对象。它是 类的所有实例共享的,在内存中只有一份副本
(2)由于 static 成员变量的值是可变的,它的初始值通常会在程序运行时确定。因此,编译器不能在类内初始化它
(3)为了确保所有类的实例共享同一份内存空间,并避免在类内重复分配存储空间,我们需要在类外部初始化它
static const 成员变量的初始化
class Apple {
public:
static const int apple_number = 10; // 可以在类内初始化
};
为什么 static const 成员变量可以在类内初始化?
常量值:const 成员变量的值是编译时常量,因此可以直接在类内进行初始化。编译器会在编译时将其值确定下来,并将其存储在合适的位置(通常是常量池)。
内存管理:由于 static const 的值在编译时已知,编译器可以处理它们在类内初始化的情形,而无需担心运行时的内存分配问题。
(1)static const 成员变量也是 类所有实例共享的,但它的值是 常量且不可修改。
(2)由于 const 确保了值是常量,并且 在编译时可确定,编译器允许它在类内进行初始化。常量的值会在编译时处理,不需要在运行时分配
(3)存储空间,因此可以直接在类内初始化。
编译时和运行时
编译时和运行时是程序执行过程中两个不同的阶段,它们的区别在于何时以及如何进行代码的处理、优化、计算等。简单来说:
编译时是程序源代码被编译器转换为机器代码的阶段,通常发生在程序运行之前。
运行时是程序开始执行(即运行)后的阶段,发生在程序实际运行时
编译时计算与程序运行时计算
- 编译时确定(Compile-time constant)
编译时常量指的是那些其值在编译阶段就已经确定了的常量。这些常量的值不会在程序运行时发生改变。编译器可以在编译时完全知道它们的值,并且将这些常量嵌入到程序的二进制文件中(如常量池、数据段等),这样在程序运行时不需要进行计算或分配额外的内存。
//apple.h
class Apple {
public:
static const int apple_number = 10; // 这是一个编译时常量
};
//apple_number 在编译时就已确定为 10,所以编译器可以将它嵌入到程序中,不需要在程序运行时进行任何计算。
//由于 apple_number 是 const,编译器知道它不会被改变,所以它可以优化其存储和访问
- 运行时确定(Run-time constant)
运行时常量指的是那些值在编译时不能确定,必须等到程序运行时才能计算出或获得的常量。它们的值在程序开始运行后才会确定,可能取决于运行时输入、程序的状态或动态计算的结果,需要额外分配内存。
//apple.h
class Apple {
public:
static int apple_number; // 这是一个运行时常量
};
//apple.cpp
// 类外初始化
int Apple::apple_number = 10;
//main.cpp
int main() {
Apple::apple_number = 20; // 可以在运行时修改
}
1.13 练习2.27 常量引用、常量指针、指向常量的指针的初始化
(a) int i = -1, &r = 0;
引用初始化应该是变量吧,非法
答案:非常量int不能引用字面值常量,只有const int才能初始化为字面值
(b) int *const p2 = &i2;
指针*const p2保存i2的地址,该地址指向int类型值,合法
(c) const int i = -1, &r = 0;
i初始化为常量整型,引用初始化应该是变量吧,一半合法,一半错误(我的答案有误)
答案:合法,r为常量引用
const int &r = 0; (常量引用可以引用字面值常量)
int &r = 0;(非常量引用不可以引用字面值常量)
(d) const int *const p3 = &i2;
指针*const p3保存i2的地址,该地址指向const int类型值,即变量i2的值无法更改,合法
(e) const int *p1 = &i2;
指针p1保存i2的地址,该地址指向const int类型值,合法
(f) const int &const r2;
非法(自己不知道理由)
理由:不能让引用恒定不变
(g) const int i2 = i, &r = i;
i值赋值给const int i2, i值赋值给const int &r,r对i进行引用,合法
1.14 练习2.28 常量引用、常量指针、指向常量的指针的定义及区别
(a) int i 整型i、int *const cp; 常量指针cp,指向整型变量,合法(我的答案错误)
答案:常量指针必须初始化
(b) p1指针指向整型变量、int *const p2; 常量指针p2,指向整型变量,合法(我的答案错误)
答案:常量指针必须初始化
(c) 常量整型变量ic、常量引用r未定义变量,非法
(d) 常量指针p3指向常量整型变量,合法(我的答案错误)
答案:常量指针必须初始化
(e) 指针p指向常量整型变量,合法
1.14 练习2.29 常量引用、常量指针、指向常量的指针的赋值方法
(a)const int ic; int i;
i = ic; 合法
(b) int *p1; const int *const p3;
p1 = p3; 非法
(c) int *p1; const int ic;
p1 = ⁣ 非法
(d) const int *const p3; const int ic;
p3 = ⁣ 合法(我的答案错误)
常量指针p3不能被赋值
(e) int *const p2; int *p1;
p2 = p1; 合法(我的答案错误)
常量指针p2不能被赋值
(f) const int ic; const int *const p3;
ic = *p3; 合法(我的答案错误)
常量ic不能被赋值
1.15 练习2.30 顶层const和底层const的区别
const int v2 = 0;
int v1 = v2;
int *p1 = &v1, &r1 = v1;
//v1为整型0,整型指针p1存储v1地址,p1指向可变,v1不变,底层const
//v1为整型0,整型引用r1相当于v1的别名,由于v1不变,相当于底层const
答案是关于p2和r2的
const int *p2 = &v2, *const p3 = &i , &r2 = v2;
//p2指针所指对象无法被更改,底层const
//p3常量指针,顶层const
//r2引用,所引用对象v2为常量,底层const
知识补充
顶层 const:限制的是变量本身——“它不能被重新赋值”。
底层 const:限制的是变量指向的对象或内容——“它的值不能被修改”。
int a = 10;
const int b = 20;
const int *p1 = &a; // p1 是底层 const
int *const p2 = &a; // p2 是顶层 const
const int *const p3 = &b; // p3 既是顶层 const,也是底层 const
// *p1 = 30; // 错误,p1 是底层 const,不能修改指向内容
p1 = &b; // 正确,可以修改 p1 的指向
*p2 = 40; // 正确,p2 的内容不是 const
// p2 = &b; // 错误,p2 是顶层 const,不能修改指向
// *p3 = 50; // 错误,p3 是底层 const,不能修改指向内容
// p3 = &a; // 错误,p3 是顶层 const,不能修改指向
1.16 练习2.31 顶层const和底层const对于拷贝操作的影响
const int v2 = 0;
int v1 = v2;
int *p1 = &v1, &r1 = v1;
//r1 = v2; v2为常量整型,r1为整型引用,合法
const int *p2 = &v2
//p1 = p2; p2底层const,p2所指变量不能改变值,p1为普通指针,p1可能修改p2所指变量的值
//p2 = p1; p1为普通指针,p2底层const,p2所指变量不能改变值,合法
int i;
const int *const p3 = &i;
//p1 = p3; p3即是顶层const又是底层const, p1为普通指针,p1可能修改p3所指变量的值,非法
//p2 = p3; p3即是顶层const又是底层const,p2底层const,p2所指变量不能改变值,合法