2-1 C++语言基本特性

前言

本章的内容聚焦于C++程序设计语言的基本特性,属于较为基础且常考的知识,每节针对1-2个基础特性进行讲解和举例,并在每节的最后将面试所涉及的考点进行总结。基础较为薄弱的同学需要全面深入理解这部分基础内容,基础较扎实的同学可以选择性的回顾。

1. 指针与引用

1.1 变量与内存地址

变量(variable)标识一块内存区域,变量值被设置在这块内存区域中,变量拥有自己的名字,变量名用于标识变量存储的内存空间。 例如,当我们声明一个int型变量x时,x是变量名,它的内存地址是0x0000。在程序中使用变量x时经过两个步骤:(1)通过符号表找出与变量x相对应的内存地址。(2)根据内存地址取出该地址对应的内存值进行操作。 图片说明

1.2 指针

指针一般是指针变量,指针变量存储的是其指向的对象的首地址。按照指向对象的类型划分,指针可以指向变量、数组、函数等具有内存地址的实体,但不同类型的指针变量所占用的内存空间大小是相同的,在32位系统下为4Byte。由于指针变量自身是变量,因此它的变量值是可以改变的,这使得指针变量可以更换指向的对象。

指针是C/C++语言特有的内存操作机制,C/C++程序员可以使用指针对变量内存地址进行动态内存分配等灵活性操作,这也给予了C/C++程序员极大的内存操作自由。

1.2.1 变量指针

如代码所示,定义int型变量x与int型指针ptr,使用连字号(&)运算符访问变量x的内存地址并赋值给ptr。同理,对于结构体类型同样适用。

int x = 0;
int *ptr = &x;  // int类型指针ptr的值为变量x的内存地址
printf("x=%d", *ptr); // 通过使用运算符 * 来返回指针所指定地址的变量值
struct student;
student stu;
student *ptr = &stu;  // 自定义结构体类型指针ptr的值为变量stu的内存地址

1.2.2 数组名与指针

众所周知,C/C++语言中数组名是数组的首地址,数组名与指针概念较为相似,因此我们可以取数组名并赋值给一个指针,这样可以通过指针来访问数组成员变量。

int array[6] = {1,2,3,4,5,6};
int *ptr = array;
printf("array[0]=%d", *ptr); // ptr为数组的首地址,使用*操作符将返回数组的第一个元素值
// 指针支持++、--、+、-四种运算
printf("array[1]=%d", *(ptr++)); // *(ptr++)将返回数组第二个元素值,此时ptr也指向了数组的第二个元素地址

注意:数组名与指针较为相似,但也有不同之处:

  • (1)数组名是指针常量,指针是指针变量;
  • (2)使用sizeof计算变量占用地址空间大小时,对数组名使用sizeof得到整个数组素有元素占用的字节,而对指针sizeof则得到指针变量占用的字节数(32位系统下为4字节)。同理,使用&操作数组名和指针时意义也不同,此时数组名也代表了数组的整体,而非普通的常量指针。

1.2.3 函数指针

众所周知,程序中定义的函数在编译时也会为函数分配存储空间,函数名为这段空间的首地址,因此我们可以定义一个指针来存储函数首地址,即函数指针。与变量指针定义不尽相同的是,函数指针的定义需要声明函数的返回值和参数列表:函数返回值类型 (* 指针变量名) (函数参数列表)。

int func(int x);   // 声明一个函数
int (*ptr) (int x);  // 定义一个函数指针, 声明返回值为int类型,参数为int类型
ptr = Func;          // 将func函数的首地址赋给指针变量ptr

// 调用
int x = 10;
int y = (*ptr)(x);  // 通过函数指针调用func函数

1.2.4 指针作为函数参数/返回值

众所周知,函数在调用和返回过程中存在形参到实参的拷贝和返回值拷贝,且函数在压内存栈的过程中实参和返回值会压入栈中,因此如果如果函数参数和返回值占用内存较大,会导致内存拷贝效率低并耗费大量栈内存。为了避免这一问题,C++允许将指针作为参数进行函数调用,指针变量占用内存大小是固定的,这极大的提高了函数调用的计算效率和内存消耗。

// 使用int类型指针作为参数
void func(int* ptr);
struct student;
// 使用结构体指针作为参数
void func(student* ptr);
// 使用结构体指针作为参数和返回值
// struct* func(student* ptr);

1.2.5 指针使用的安全性

  • 指针在定义时一定要初始化,可以为空指针(NULL/nullptr),若未初始化则会随机指向一块内存区域。
  • 指针的操作注意变量作用域,例如在使用指针访问数组成员时,若访问越界则会产生无法预料的结果;不要在函数返回时传递栈空间的指针(函数定义的局部变量指针等),因为函数调用结束后栈空间会被释放。(函数调用的压栈过程在4.1节内存管理中讲解)
  • 指针指向的变量空间已失效(变量被销毁等),那么在使用*操作符取指针指向变量值时会产生不可预料的结果。

1.3 引用

引用(reference)可以理解为变量的别名,实际上引用是一种常量指针,引用在被创建时初始化指向一个变量,在使用引用时自动调用*操作符取得变量值。

int x;  // 声明int型变量x
int &ref = x; // 使用&声明变量x的引用ref

1.3.1 引用作为函数参数/返回值

与指针类似的,C++支持将引用作为函数参数和返回值调用。

// 使用int类型指针作为参数
void func(int& ptr);
struct student;
// 使用结构体指针作为参数
void func(student& ptr);
// 使用结构体指针作为参数和返回值
// struct& func(student& ptr);

1.3.2 关于函数调用中参数的值传递与地址传递

前文提到,函数在调用过程中存在形参到实参的拷贝,因此如果我们不使用指针或引用作为函数的参数,那么函数在执行过程中对实参的操作均是形参的拷贝,而形参变量并未参与函数的计算。

// 值传递
void add(int x, int y);
// 地址传递
void add(int *x, int *y); // 指针
void add(int &x, int &y); // 引用

使用指针和引用为程序减少的内存拷贝效率相信各位同学已经清楚了,在实践中使用指针时需要更多的考虑指针使用的安全性,在不涉及指针指向变量的改变时应尽量多的使用引用。

1.3.3 指针和引用的算数运算

指针的值是变量地址。因此,指针可以进行进行四种算术运算:++、--、+、-。并且,指针进行算数运算与指针指向变量的类型关联,若指针指向的是int型(4字节)的变量,则该指针进行算数运算都会偏移4的倍数个字节。例如有一个指针ptr指向内存地址为1000,指针为指向int型变量的指针,则ptr++运算后该指针会指向1004的位置。

引用的算数运算即是引用对象的算数运算。

1.4 面试考点

1.4.1 指针与引用的区别是什么?

【出现频度】★★★★★ 【难度】☆☆ 【参考答案】

  • 存在形式:指针是一个变量,有自己的内存空间;而引用只是一个变量的别名。因此在使用sizeof获取指针和引用的内存大小时,指针的大小是4(32bit系统下),而引用则返回被引用对象的内存大小。
  • 初始化时:指针可以被初始化为NULL,而引用必须被初始化关联一个已有对象。
  • 初始化后:指针可以被修改指向其他变量,而引用不可以改变所引用的对象。
  • 使用操作上:指针需要被解引用才可以进行变量操作,指针对于++、--、+=、-=操作符具有特殊的意义;而对引用的操作就是直接修改所引用的变量的值。
  • 其他:指针可以有多级指针(**ptr),而引用不存在多级引用。

1.4.2 指针操作对象的方式?指针作为函数参数时的地址传递介绍

【出现频度】★★★★★ 【难度】☆☆ 【参考答案】

  • 指针通过解引用操作符*对其指向的对象进行操作
  • 指针作为函数参数传递后,在函数中操作指针类型的形参进行对象的操作,也会改变实参对象;实际上,通过指针操作的是同一个对象,同一片内存空间。

1.4.3 指针和引用的算数运算

【出现频度】★★★★★ 【难度】☆☆ 【参考答案】 参考1.3.3。 本题考查的本质实际为指针自身是一个变量,它的值为它指向对象的内存空间。而引用是个别名,对引用的操作就是对原对象的操作。

2. const与static关键字

const实现了常量语义,使得编译器强制对const修饰的变量约束不可修改。若在程序设计中某个变量的值是保持不变的,那么程序应该明确使用const来强制约束。

2.1 const常量语义

2.1.1 const修饰普通变量

const int x = 0; // const修饰变量x为常量
x = 1; // 错误,x不可修改
int y = x; // 正确

变量x被const修饰为常量,x的值不可以被修改,但当尝试使用指针来修改变量x时:

const int x = 0; // const修饰变量x为常量
int *ptr = (int*)&x;
(*ptr) = 1;
cout<<x<<endl;  // 输出0
cout<<*ptr<<endl;  // 输出1

这段代码可以编译通过,但运行时我们会发现输出指针ptr指向的地址空间值是1,但输出变量x的值仍是0。这与编译器的优化有关,编译器发现定义变量x与输出变量x的代码之间没有对变量x进行修改,那么在输出x的值时候编译器会从寄存器中读取上次读变量x的值,而不是真正的去变量x所在的内存地址去取值。因此,若变量x被明确修饰为const常量时,我们不应该想方设法去修改x的值,这样会产生不可预料的后果。

为了避免上述问题,我们可以使用volatile去修饰变量x,来告知编译器这个变量值是多变的,在取变量值时编译器会从该变量的内存地址取,从而避免由于编译器优化产生的取值错误。

volatile const int x = 0; // const修饰变量x为常量
int *ptr = (int*)&x;
(*ptr) = 1;
cout<<x<<endl;  // 输出1

2.1.2 const修饰指针

常量指针:const修饰指针指向的内容,即指向常量的指针。指针指向的变量x不可通过指针p改变其值,简称左定值,即const位于*号的左边。

int x = 0;
// 以下两种写法都是常量指针
const int *p = &x;  
int const *p = &x; 

指针常量:const修饰指针,指针自身为常量,即指针指向的内存地址不可改变,但内存中存储的值可以被改变,简称右定值,即const位于*号的右边。

int x = 0; 
int* const ptr = &x; 
*ptr = 1; // 正确
int y = 0;
ptr = &y; // 错误 

指向常量的指针常量:常量指针与指针常量的合并,使用两个const修饰,指针指向不可改变,指向的内容也不可以改变。

int x = 0;
const int * const  ptr = &a;

2.1.3 const修饰函数参数和返回值

  • const修饰函数参数 如果函数参数确实是常量语义,那么应该明确使用const修饰函数参数中的指针和引用,可以避免函数在调用过程中产生不正确的篡改。
struct student;
// 在函数中对ptr指针的修改和stu1变量的修改都是不允许的
void func(const student* const ptr, const student& stu1); 
  • const修饰函数返回值 如果函数的返回值是指针类型,且使用const修饰返回指针,那么根据const修饰指针的位置进行常量限制,且该返回值只能赋值给同类型的指针。
const char * getString(); // 函数声明,返回值类型是char类型的常量指针
char* pStr = getString(); // 错误,不能将const char* 赋值给char*类型变量
const char *pStr = getString(); // 正确

2.1.4 const修饰类成员函数

const修饰类成员函数作用是限制成员函数不能修改成员变量的值,但const不能与static共同修饰类的成员函数,因为static修饰的类成员函数在调用时不通过this指针不能实例化,const成员函数必须关联到具体的实例。

class student 
{
    public:
        student(std::string name) : m_name(name) {}
        std::string getName() const
        {
            // 在这一成员函数中不允许修改成员变量的值
            return m_name;
        }
    private:
        std::string m_name;
};

2.1.5 面试考点: const关键字的作用和意义是什么?

【出现频度】★★★★ 【难度】☆ 【参考答案】 const关键字实现了常量语义,在程序设计中一些不会发生变化或不应被修改的变量应明确使用const进行常量修饰,以取得编译器的帮助。const用来修饰普通变量为常量不被修改;const修饰指针变量,根据const所在的不同位置限制指针指向的修改和指向变量被修改的能力;const用来修饰函数参数和返回值,函数在调用过程中const参数不会被修改;const修饰类的成员函数,该成员函数在调用过程中无法修改成员变量的值。

2.2 static静态语

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++岗面试真题解析 文章被收录于专栏

<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>

全部评论
实参和形参是不是写反了呀
点赞 回复 分享
发布于 2022-06-02 16:13
printf("array[1]=%d", *(ptr++)); // *(ptr++)将返回数组第二个元素值,此时ptr也指向了数组的第二个元素地址 这里错了,应改为 printf("array[1]=%d", *(++ptr)); // *(++ptr)将返回数组第二个元素值,此时ptr也指向了数组的第二个元素地址
点赞 回复 分享
发布于 04-19 11:18 广东

相关推荐

头像
10-09 19:35
门头沟学院 Java
洛必不可达:java的竞争激烈程度是其他任何岗位的10到20倍
点赞 评论 收藏
分享
孤寡孤寡的牛牛很热情:为什么我2本9硕投了很多,都是简历或者挂,难道那个恶心人的测评真的得认真做吗
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务