【常问基础】01.关键字

【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM

【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym

【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM

【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0

alt

01.关键字

static

01.关键字static的作用

一般来说

1.static的第一个作用是隐藏。(static函数,static变量均可)

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

2.static的第二个作用是变量只初始化一次,保持变量内容的持久。(static变量中的记忆功能和全局生存期)

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

3.static的第三个作用是默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

static的第一个作用具体来说

  • 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,在函数体,只会被初始化一次,因此其值在下次调用时仍维持上次的值;

  • 模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问(只能被当前文件使用);

  • 模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内(只能被当前文件使用);

  • 类中的static成员变量

    属于整个类所拥有,只与类关联,不与类的对象关联;

    定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;

    可以被非static成员函数任意访问。

  • 类中的static成员函数

    属于整个类所拥有,不具有this指针;

    无法访问类对象的非static成员变量和非static成员函数;

    不能被声明为const、虚函数和volatile;

    可以被非static成员函数任意访问。

类内static相关说明:

类中的static成员变量

在类内的数据成员声明前加上关键字static,则该成员将会被声明为静态数据成员.

#include <bits/stdc++.h>
using namespace std;

class TempClass
{
public:
	TempClass(int a, int b, int c);
	void Show();
private:
	int a,b,c;
	static int T;
};
int TempClass::T = 0;//初始化静态数据成员
TempClass::TempClass(int a, int b, int c)
{
	this->a = a;
	this->b = b;
	this->c = c;
	T = a + b + c;
}
void TempClass::Show()
{
	printf("T is %d\n", T);
}
int main()
{
	TempClass ClassA(1,1,1);
	ClassA.Show();//输出1+1+1 = 3;
	TempClass ClassB(3,3,3);
	ClassB.Show();//输出3+3+3 = 9;
	ClassA.Show();//输出9
	return 0;
}

从上面的测试代码可以看出静态数据成员的特点:

  • 静态数据成员的服务对象并非是单个类实例化的对象,而是所有类实例化的对象(这点可以用于设计模式中的单例模式实现).
  • 静态数据成员必须显示的初始化分配内存,在其包含类没有任何实例化之前,其已经有内存分配.或者说这个变量只属于这个类,不属于任何类实例化的对象,不能用某个对象去赋值,所以static修饰的变量要在类外初始化
  • 静态数据成员与其他成员一样,遵从public,protected,private的访问规则.
  • 静态数据成员内存存储在全局数据区,只随着进程的消亡而消亡.

静态数据成员与全局变量相比的优势:

  1. 静态数据成员不进入程序全局名字空间,不会与其他全局名称的同名同类型变量冲突.
  2. 静态数据成员可以实现C++的封装特性,由于其遵守类的访问权限规则.所以相比全局变量更加灵活.

类中的static成员函数

在类的成员函数返回类型之前添加static,即可声明此成员函数为静态成员函数.

#include <stdio.h>
class TempClass
{
public:
	TempClass(int a, int b, int c);
	static void Show();
private:
	int a,b,c;
	static int T;
};
int TempClass::T = 0;	//初始化静态数据成员
TempClass::TempClass(int a, int b, int c)
{
	this->a = a;
	this->b = b;
	this->c = c;
	T = a + b + c;
}
void TempClass::Show()
{
	printf("T is %d\n", T);
}
int main()
{
	TempClass ClassA(1,1,1);
	ClassA.Show();
	TempClass ClassB(3,3,3);
	ClassB.Show();
	
	TempClass::Show();	//注意此处的调用方式.
	return 0;
}

从上面的示例代码中可以看出 静态成员函数的特点如下:

  • 静态成员函数比普通成员函数多了一种调用方式.
  • 静态成员函数为整个类服务,而不是具体的一个类的实例服务.
  • 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员

关于this指针的深入解释

在C++中,this指针是一个隐含的指针,它指向当前对象的地址。当一个成员函数被调用时,编译器会将该函数的调用对象的地址作为this指针传递给函数。例如调用函数Fun(),实际上是this->Fun().静态成员函数中没有这样的this指针,所以静态成员函数不能操作类中的非静态成员函数.否则编译器会报错.

类内的静态成员函数不能被声明为 const、虚函数或 volatile 的原因如下:

const

const 修饰的成员函数是指其在函数内部不会修改对象的状态。这意味着该成员函数只能访问对象的 const 成员,而不能访问非 const 成员。由于静态成员函数不依赖于任何对象,因此它们不能声明为 const。在 C++ 中,编译器会在静态成员函数上使用 const 修饰符时给出编译错误。

虚函数

虚函数是指在基类中声明的函数,在派生类中可以被重写。static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function。

volatile

volatile 修饰的变量是指该变量可能在任何时候都会被修改,因此编译器不会将该变量缓存在寄存器中,而是每次都从内存中读取该变量的值。然而,volatile 不适用于类的静态成员函数,因为静态成员函数没有隐含的 this 指针,也就是说它没有与特定对象实例相关联。而 volatile 关键字的主要目的是告诉编译器不要对变量进行优化,确保每次访问都是从内存中读取最新值,但对于静态成员函数来说,并不存在与对象实例相关的内存位置。静态成员函数不依赖于任何对象,只能对静态成员变量做修改,不涉及任何其他对象变量的修改,因此不能声明为 volatile。在 C++ 中,编译器会在静态成员函数上使用 volatile 修饰符时给出编译错误。

在类的静态成员函数中,无法使用 volatile 来强制编译器在每次调用时都重新读取某个特定对象实例的数据,因为静态成员函数是针对类本身而不是类的实例的。如果需要使用 volatile 行为来处理类的静态数据,你可以在静态数据成员上使用 volatile 关键字,而不是将其应用于整个静态成员函数。

static的第二个作用是变量只初始化一次,保持变量内容的持久

示例

#include <stdio.h>

void func() {
    static int staticVar=0; // 静态变量,只初始化一次。
    printf("Static variable: %d\n", staticVar);
    staticVar++; // 每次调用函数,静态变量增加1
}

int main() {
    func(); // 第一次调用
    func(); // 第二次调用
    func(); // 第三次调用

    return 0;
}
输出
Static variable: 0
Static variable: 1
Static variable: 2
02.在C语言中,为什么 static变量只初始化一次?

C/C++程序的内存布局,引申到为什么static的生命周期是到程序结束?

在C语言中,静态变量在程序的生命周期内只会被初始化一次。这是因为静态变量是在程序编译时就被分配了内存空间,存放在静态区,并且其值在程序执行期间都会保持不变,所以只需要在第一次初始化时进行赋值即可。

当程序运行时,静态变量的值会一直保留在内存中,直到程序结束。如果程序中有多个地方使用了同一个静态变量,那么它们都会访问同一个内存地址中的值。

这种行为与自动变量不同。自动变量的内存空间是在函数调用时动态分配的,存放在栈区,并且每次函数调用时都会重新初始化。因此,自动变量的值在函数调用结束后就会被销毁,而静态变量的值则会一直保留在内存中。

03.静态变量什么时候初始化

初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。

在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

04.简述static对于工程模块化的作用。

在工程模块化中,使用 static 关键字可以起到控制变量、函数和对象的作用域的作用,从而提高代码的可维护性和可重用性。

具体来说,使用 static 可以使得变量、函数和对象仅在当前模块中可见,而不会影响其他模块中的同名变量、函数和对象。这样做的好处是可以减少不同模块之间的耦合性,提高代码的可维护性和可重用性。因为静态变量和函数只在当前模块中可见,所以它们不会被其他模块意外地修改或调用,从而减少了程序出错的可能性。

此外,使用 static 还可以避免变量和函数名称的冲突。在工程中可能存在多个模块,这些模块可能都有相同的变量和函数名,使用 static 可以避免这种命名冲突。这样做也使得代码更加模块化,更容易阅读和理解。

05.为什么不能在头文件定义static变量。

可以通过编译,但是使用static定义变量证明该变量的作用域范围仅在该源文件内,其他源文件不能访问。如果在头文件中定义static变量,证明包含了所有这个头文件的源文件都定义了该变量,会造成变量多次定义浪费内存,而且不是真正的全局变量

06.定义1MB静态全局数组,且未初始化,会对elf有明显的增加吗?

不会,只增加一个指针的大小指向未初始化/初始化为0区域,在实际开发量产中是非常有用的,这种数据定义不会增加flash空间占用

extern

07.extern作用

可以在一个文件中引用另一个文件中定义的变量或者函数

extern关键字只需要指明类型和变量名就行了,不能再重新赋值

  1. 引用同一个文件中的变量利用extern关键字,使用在后边定义的变量

    #include<stdio.h>
    int func();
    int main()
    {
        func(); //1
        extern int num;
        printf("%d",num); //2
        return 0;
    }
    int num = 3;
    int func()
    {
        printf("%d\n",num);
    }
    
  2. 引用另一个文件中的变量 使用include将另一个文件全部包含进去可以引用另一个文件中的变量,但是这样做的结果就是,被包含的文件中的所有的变量和方法都可以被这个文件使用,这样就变得不安全,如果只是希望一个文件使用另一个文件中的某个变量还是使用extern关键字更好。

    main.c
    #include<stdio.h>
    int main()
    {
        extern int num;
        printf("%d",num);
        return 0;
    }
    
    b.c
    #include<stdio.h>
    intnum = 5;
    voidfunc()
    {
        printf("fun in a.c");
    }
    
  3. 引用另一个文件中的函数

    main.c
    #include<stdio.h>
    int main()
    {
        extern void func();
        func();
        return 0;
    }
    
    b.c
    #include<stdio.h>
    const int num=5;
    void func()
    {
    	printf("fun in a.c");
    }
    
08.extern ”C” 的作用

在 C++ 中,extern "C" 用于指定某个函数、变量、代码块等按照 C 语言的规则进行编译和链接,以便与 C 语言代码进行互操作。

当使用 C++ 编译器编译 C++ 代码时,编译器会将函数名进行名称修饰以支持函数重载等特性。而 C 语言并不支持函数重载,函数名也不进行名称修饰。因此,当我们在 C++ 代码中调用 C 语言中的函数时,需要使用 extern "C" 来告诉编译器不要对这个函数名进行名称修饰,以便与 C 语言代码进行互操作。

另外,extern "C" 也可以用于解决 C++ 代码在链接时找不到符号的问题。在 C++ 中,如果我们定义了一个函数或变量但没有给它赋初值,那么编译器会将这个函数或变量放在未初始化数据段(BSS)中,而不是在已初始化数据段(DATA)中。然而,在链接时,如果其它模块中没有找到这个符号,链接器会报错。这时,我们可以使用 extern "C" 来告诉编译器不要对这个符号进行名称修饰,以便在链接时能够找到对应的符号。

示例

  • C++调用C函数:
//xx.h
extern int add(...)

//xx.c
int add(){
    
}

//xx.cpp
extern "C" {
    #include "xx.h"
}
  • C调用C++函数
//xx.h
extern "C"{
    int add();
}
//xx.cpp
int add(){    
}
//xx.c
extern int add();

const

const有哪些作用_shangtang的博客-CSDN博客_const的作用

09.const作用

1)定义const只读变量,具有不可变性防止被意外修改

2)进行类型检查,使编译器对处理内容有更多了解,消除一些隐患。

3)避免意义模糊的数字出现,同样可以很方便地进行参数检查和修改。同宏定义一样,可以做到不变则已,一变都变。

4)为函数重载提供参考

5)节省空间,避免不必要的内存分配。const定义的常量在程序运行过程中只有一份复制品,而#define定义的常量在内存中有若干个复制品。const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而#define定义的常量在内存中有若干个拷贝。

#define PI 3.14159        //常量宏 
const doulbe Pi=3.14159;  //此时并未将Pi放入ROM中 ...... 
double i=Pi;              //此时为Pi分配内存,以后不再分配! 
double I=PI;              //编译期间进行宏替换,分配内存 
double j=Pi;              //没有内存分配 
double J=PI;              //再进行宏替换,又一次分配内存!

6)提高了程序的运行效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,使得它成为一个编译期间的常量,没有了存储和读内存操作,使得它的运行效率也很高。

C语言中,定义的const将重新分配一个内存空间。

C++中,编译器对const做了特殊处理,将const常量放到符号表中。

10.什么情况下使用const关键字?

1)修饰一般常量。在定义时必须初始化,之后无法更改

2)修饰常数组。

3)修饰常指针。

2-c语言之const详解_哔哩哔哩_bilibili

const int *a;             
int const *a;              // a是一个指向整型常量的指针变量,指针所指向的内容只读 

int * const a;             // a是一个指向整型变量的指针常量,指针本身是只读的  

const int * const a = &b;
int const * const a = &b;  // a是一个指向整型常量的指针常量,指针所指向的内容只读且指针本身是只读的

4)修饰函数的常参数。

  • 如果函数参数采用“指针传递”,那么加 const 修饰可以防止意外地改动该指针指向的内容,起到保护作用。
char *strcpy(char *strDest, const char *strSrc);  // 参数在函数内部不会被修改 
  • const 用于修饰“指针传递”的参数,以防意外改动指针本身,C++引用的原型。
void swap ( int * const p1 , int * const p2 );
  • 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加 const 修饰。
例如不要将函数 void Func1(int x) 写成 void Func1(const int x);

5)修饰函数的返回值。

  • 如果给用 const修饰返回值的类型为指针,那么函数返回值(即指针)的内容是不能被修改的, 而且这个返回值只能赋给被 const修饰的指针。
const char *GetString() //定义一个函数 
char *str= GetString()  //错误,因为str没有被const修饰 
const char *str=GetString() //正确
  • 如果用 const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函数调用结束后这个临时变量的生命周期也就结束了,因此把这些返回值修饰为 const是没有意义的。
int GetInt(void);
const int GetInt(void);

6)修饰常引用。

  • 引用变量
变量初始化,再const引用变量
	int b = 10;
	const int &a = b;
	b = 11;//b是可以修改的,但是a不能修改
  • 引用常量
const引用常量
	const int &c = 15;
	//编译器会给常量15开辟一片内存,并将引用名作为这片内存的别名
	//int &d=15//err
  • 用于函数的形参。常引用做形参,可以确保在函数内不会改变实参的值,所以参数传递时尽量使用常引用类型。

7)修饰类的成员变量

不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。

8)修饰类的成员函数

const对象不可以调用非const成员函数;非const对象都可以调用;

不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。

class People
{
 public:
    int talk(void);
    int eat(void) const; // const 成员函数
 private:
    int m_age;

};
int People::eat(void) const
{
    ++m_age; // 编译错误,企图修改数据成员m_num
    talk();  // 编译错误,企图调用非const函数
    return    m_age;
}

9)修饰常对象。定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员了(包括 const 成员变量和 const 成员函数),因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。

// show是普通成员函数,get是const成员函数
int main(){
    const Student stu("小明", 15, 90.6);
    //stu.show();  //error
    cout<<stu.getname()<<"的年龄是"<<stu.getage()<<",成绩是"<<stu.getscore()<<endl;
    const Student *pstu = new Student("李磊", 16, 80.5);
    //pstu -> show();  //error
    cout<<pstu->getname()<<"的年龄是"<<pstu->getage()<<",成绩是"<<pstu->getscore()<<endl;
    return 0;
}
本例中,stu、pstu 分别是常对象以及常对象指针,它们都只能调用 const 成员函数
11.const 与define区别

定义常量谁更好?# define还是 const?

define与 const都能定义常量,效果虽然一样,但是各有侧重。 define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而 const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下几个方面。

  1. 编译器处理方式不同

    define宏是预编译指令,在预处理阶段展开。

    const常量是普通变量的定义,编译运行阶段使用。

  2. 存储方式不同

    define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)

    const常量会在内存中分配(可以是堆中也可以是栈中),const 可以节省空间,避免不必要的内存分配。

    const定义的是变量,而define定义的是常量。define定义的宏在编译后就不存在了,它不占用内存,因为它不是变量,系统只会给变量分配内存。但const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。可以说,常变量是有名字的不变量,而常量是没有名字的。有名字就便于在程序中被引用,所以从使用的角度看,除了不能作为数组的长度,用const定义的常变量具有宏的优点,而且使用更方便。所以编程时在使用const和define都可以的情况下尽量使用常变量来取代宏。

  3. 类型和安全检查不同

    define宏没有类型,不做任何类型检查,仅仅是展开。容易出问题,即“边际问题”或者说是“括号问题”。

    const常量有具体的类型,在编译阶段会执行类型检查。

  4. const可以调试

    const 只读变量是可以进行调试的,define 是不能进行调试的,因为在预编译阶段就已经替换掉了。

一般问什么和什么的区别,可以从存储方式、编译阶段、类型检查、可否调试、应用对象、作用域这几方面来考虑说明就行。

记忆:存编型,调对域

12.一个参数既可以是const还可以是volatile吗?

是的。一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改变,它是const因为程序不应该试图去修改它。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

volatile修饰符告诉complier变量值可以以任何不被程序明确指明的方式改变,另一个例子就是外部端口的值,它的变化可以不用程序内的任何赋值语句就有可能改变的,这种变量就可以用volatile来修饰,编译器不会优化掉它。 const修饰的变量在程序里面是不能改变的,但是可以被程序外的东西修改,就像上面说的外部端口的值,如果仅仅使用const,有可能complier会优化掉这些变量,加上volatile就万无一失了。

例如:

const volatile int* ptr;

这个声明表示 ptr 是一个指向 const volatile int 类型的指针,也就是说,ptr 所指向的变量不能被修改,并且它的值可能会在程序执行期间被意外修改。

同样地,一个函数参数也可以是 constvolatile,例如:

void foo(const volatile int* ptr);

constvolatile 的顺序可以交换,即 volatile const int* 也是合法的类型声明。

13.C++的顶层const和底层const

概念区分

  • 顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
  • 底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
int a = 10;int* const b1 = &a;      //顶层const,b1本身是一个常量
const int* b2 = &a;                 //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; 		            //顶层const,b3是常量不可变
const int* const b4 = &a;           //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a;		            //用于声明引用变量,都是底层const

区分作用

  • 执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const
  • 使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const

volatile

14.volatile作用

声明变量是易变,避免被编译器优化

声明volatile后 //编译器就不会优化,会从内存重新装载内容,而不是直接从寄存器拷贝内容(副本) //否则会优化,会读寄存器里的副本,而重新读内存(因寄存器比内存快)

15.volatile用法

**读硬件寄存器时(如某传感器的端口/裸机程序编写时)**并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,告诉编译器不要对存储在这个地址的数据进行假设。

//假设某烟雾传感器的 硬件寄存器如下(当又烟雾时报警变为1)
#define GPA1DAT (*(volatile unsigned int*)0xE0200084) 

void main(){
 while (1){//反复读取GPA1DAT值,当为1时火灾报警
    if (GPA1DAT) {  //如不加volatile,编译器优化后,变成只读一次,
                    //后面用的是副本数据。一直为0
	fire()      
	break;
     }
 }
}

解释

1.#define GPA1DAT (*(volatile unsigned int*)0xE0200084) 
将 GPA1DAT 宏定义为地址 0xE0200084 上的内容

2.(volatile unsigned int*)0xE0200084
将0xE0200084强制转换为地址int型指针(相当于*p中的p,指的是地址)

3.(*(volatile unsigned int*)0xE0200084)
这句代码则代表地址为0xE0200084上的存放内容(相当于*p,指的是地址上的内容)

4.#define GPA1DAT (*(volatile unsigned int*)0xE0200084) 
将地址0xE0200084上的内容定义为GPA1DAT,如果操作GPA1DAT = 1;则地址0x40000000上存放的内容就变成了1,也可以读.
//裸机程序编写时
//main.c
#define CNF	    (*(volatile  int*)0x6000D204) //配置寄存器 (0:GPIO  1:SFIO)
#define OE   	(*(volatile  int*)0x6000D214) //输出使能寄存器 (1:使能 0:关闭)
#define OUT     (*(volatile  int*)0x6000D224) //输出寄存器(1:高电平 0:低电平)

#define MSK_CNF (*(volatile  int*)0x6000D284) //配置屏蔽寄存器(高位1:屏蔽 高位0:不屏蔽 低位1:GPIO模式 低位0:SFIO模式)
#define MSK_OE  (*(volatile  int*)0x6000D294) //输出使能屏蔽寄存器(高位1:禁止写   低位1:使能)
#define MSK_OUT (*(volatile  int*)0x6000D2A4) //输出屏蔽寄存器(高位1:禁止写   低位1:高电平)

#define DAP4_SCLK_PJ7 (*(volatile  int*)0x70003150)//管脚复用

//开灯
void led_on(void)
{
 //管脚复用
 DAP4_SCLK_PJ7 = DAP4_SCLK_PJ7&(~(1 << 4));             //【位清零程序写法】
 //取消GPIO3_PJ7 引脚的屏蔽
 MSK_CNF = (MSK_CNF)|(1<<7); //取消对GPIO模下引脚的屏蔽    //【位置一程序写法】
 MSK_OE = (MSK_OE)|(1<<7); //取消引脚 使能屏蔽
 //配置GPIO3_PJ7 引脚  输出高电平
 CNF = (CNF)|(1<<7);  //配置引脚为 GPIO模式
 OE = (OE)|(1<<7);    //使能引脚
 OUT = (OUT)|(1<<7);  //引脚输出高电平,点亮灯
}

int main(void) 
{ 
 led_on();

 while(1)
 {
 }
 return 0;
}

中断中对共享变量的修改一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

static int i=0; //应加volatile修饰
int main(void)
{
  ...
  while (1){
  if (i) { //虽中断中更改了i的值,但因未声明i是易变的,
 	      //编译器优化后,导致它读的是副本数据,导致一直循环不退出
    break;
  }
 }
}
void interrupt(void)
{
   i=1;  //中断中改变 i的值,但
}

多线程中对共享的变量的修改多线程应用中被几个任务共享的变量。单地说就是防止编译器对代码进行优化,比如如下程序:

volatile  char  bStop  =  0;  //注意:需声明为volatile,线程而才能通过它停止线程1
                          //如不声明,编译器优化后,变成一直读副本数据。
void thread1(){  
  while(!bStop)  {
    //...一直循环做一些事情
  }  
}
void thread2(){  
   //...处理一些事情后。
   bStop =1; //终止线程2  
} 
16.一个指针可以是volatile吗?

是的。一个例子是当一个中断服务子程序修改一个指向一个缓冲区的指针时

指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整形数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。

17.下面的函数有什么问题?
int square(volatile int *ptr)   
{
    return *ptr * *ptr;   
}  

这个函数的目的是用来返回指针* ptr指向值的平方,但是,由于* ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)   
{   
    int a, b;   
    a = *ptr;   
    b = *ptr;   
    return a * b;   
} 

由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)   
{   
    int a;   
    a = *ptr;   
    return a * a;   
} 
18.C语言编译过程中,关键字volatile和extern分别在哪个阶段起作用?

C语言编译过程分为预处理、编译、汇编、链接。

  1. volatile关键字: 在编译阶段volatile关键字会告诉编译器该变量可能被意外地修改,因此编译器不会进行优化,以确保每次读取该变量的值都是最新的。
  2. extern关键字: 在编译阶段,extern关键字会告诉编译器该变量或函数在其他文件中定义,因此编译器不会在当前文件中分配存储空间,而是在链接阶段将其与其他文件中的定义关联起来,从而实现跨文件的共享使用。

sizeof和strlen

19.sizeof和strlen有什么区别
  1. sizeof()是运算符(既是关键字,也是运算符,但不是函数),strlen()是库函数
  2. sizeof()在编译时计算好了,strlen()在运行时计算
  3. sizeof()计算出对象使用的最大字节数,strlen()计算字符串的实际长度

sizeof都是在编译的时候计算的,所以可以通过 sizeof(x)来定义数组维数。 strlen则是在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小。

例如, char str[20] = "0123456789”,字符数组str是编译期大小已经固定的数组,在32位机器下,为 sizeof(char)*20=20,而其 strlen大小则是在运行期确定的,所以其值为字符串的实际长度10。

  1. sizeof()的参数类型多样化(数组,指针,对象,函数都可以),strlen()的参数必须是字符型指针,而且必须是以“\0结尾的(传入数组时自动退化为指针)

当参数分别如下时,sizeof返回的值表示的含义如下: 数组——编译时分配的数组空间大小; 指针——存储该指针所用的空间大小(在32位机器上是4,64位机器上是8); 类型——该类型所占的空间大小; 对象——对象的实际占用空间大小; 函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。

记忆:运运大类

20.不使用 sizeof,如何求int占用的字节数?
#include <stdio.h> 
#define MySizeof(Value) (char *)(&value+1)-(char*)&value 
int main() {
    int i; 
    double f; 
    double *q; 
    printf("%d\r\n",MySizeof(i)); 
    printf("%d\r\n",MySizeof(f)); 
    printf("%d\r\n",MySizeof(a)); 
    printf("%d\r\n",MySizeof(q)); 
    return 0;
}
输出
4 8 32 4

上例中,(char*)& Value返回 Value的地址的第一个字节,(char*)(& Value+1)返回 value的地址的下一个地址的第一个字节,所以它们之差为它所占的字节数。

21.不能用 sizeof()函数,如何判断操作系统是16位,还是32位
  • 可以用上一题的办法16位操作系统int两字节,32位操作系统四字节。

  • 16位系统中,int变量的范围-32768到+32767,32767+1变为-32768。可以利用这个特性来判断。

22.strlen("\0") =? sizeof("\0")=?

strlen("\0") =0,sizeof("\0")=2。 strlen用来计算字符串的长度(在C/C++中,字符串是以**"\0"作为结束符的**),它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串结束符\0为止,然后返回计数器值sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。

struct和union

23.struct与 union的区别是什么?

内存分配方式(主要区别):

  • 结构体struct中的各个成员会占用不同的内存空间,因此结构体的大小为各个成员大小的总和。
  • 联合体union中的各个成员共用同一块内存空间,因此联合体的大小为各个成员中占用内存最大的那个成员的大小。

数据存储方式:

  • 结构体struct中的各个成员在内存中是按照定义的顺序依次存储的,也就是说结构体的存储顺序与定义顺序相同。
  • 联合体union中的各个成员共用同一块内存空间,存储顺序是不确定的,取决于最后一次赋值的成员。

用途:

  • 结构体struct常用于描述一组相关的数据,例如一个人的姓名、年龄、性别等信息。
  • 联合体union常用于节省内存空间,例如可以用一个联合体表示一个变量可以是不同类型的数据。

typedef和#define

24.typedef和 #define有什么区别

宏定义和typedef都可以用来定义新的类型,但是它们在以下方面有所不同。

  1. 定义方式:宏定义使用#define指令来定义,用于定义常量及书写复杂的内容,而typedef使用typedef关键字来定义。用于定义类型别名。
  2. 编译时期的差异:宏定义在预处理阶段被展开为实际的代码,而typedef则需要在编译阶段生成目标代码。
  3. 作用域:宏定义没有作用域的限制,可以在任何地方使用。typedef定义的类型名则具有作用域,只能在定义它的作用域内使用。
  4. 类型安全:使用typedef定义的类型名是一种真正的类型,可以提高程序的类型安全性。宏定义则只是简单的文本替换,不会进行类型检查。
  5. 可读性:typedef定义的类型名可以使代码更加易读易懂,因为可以通过给类型命名来表达其含义。宏定义则不具备这种优势。
  6. 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
  7. 注意对指针的操作,typedef char * p_char#define p_char char *的区别。

typedef char * p_char#define p_char char *虽然在表面上看起来很相似,但它们实际上有着不同的含义和使用方式。

typedef是C语言中的一个关键字,用于给现有类型定义一个新的名字。在这种情况下,typedef char * p_charp_char定义为一个新的类型,即指向char类型的指针。可以像使用任何其他类型一样使用p_char来声明变量,如下所示:

typedef char * p_char;
p_char str1, str2;

相比之下,#define是一个预处理器指令,用于定义常量或宏。在这种情况下,#define p_char char *p_char定义为一个宏,它的值是char *。这意味着每次在代码中使用p_char时,都会用char *来替换宏。这可能会导致一些问题,因为它不像typedef那样严格定义了一个新类型。例如:

#define p_char char *
p_char str1, str2;

这实际上是展开为以下代码:

char * str1, str2;

这意味着str1将是一个指向char的指针,而str2将是一个char类型的变量。这不是我们想要的结果。

因此,typedef是更好的选择,因为它明确地定义了一个新类型,而#define仅仅是一种简单的字符串替换。

25.关键字typedef在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。
#define  dPS  struct s *   
typedef  struct s *  tPS;  //(顺序、分号、#号) 

以上两种情况的意图都是要定义dPS 和tPS 作为一个指向结构体s的指针。哪种方法更好呢?为什么?

typedef更好。使用宏定义创建类型别名的方式不够安全,因为宏定义只是简单的文本替换,容易引入潜在的问题

举个例子:

dPS  p1, p2;   
tPS  p3, p4;

第一行代码扩展为 struct s * p1, p2; 即定义p1为一个指向结构体的指针,p2为一个实际的结构体,这也许不是你想要的。

第二行代码正确地定义了p3 和p4 两个指针。

26.关键字typedef的使用。

typedef的用法_qingtu01的博客-CSDN博客

typedef_哔哩哔哩_bilibili

  1. 为基本数据类型定义新的类型名
typedef long double REAL;//定义一个叫REAL的浮点型,表示最高精度的类型
typedef double REAL;//平台不支持long double时可改写为该种形式
typedef float REAL;//平台不支持double时可改写为该种形式
  1. 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct tagPoint Point;//为结构类型struct tagPoint定义了新的别名Point
Point p1 = { 100,100,0 };
Point p2;
  1. 为数组定义简洁的类型名称
typedef int INT_ARRAY_100[100];
INT_ARRAY_100 arr;
  1. 为指针定义简洁的类型名称
int* (*a[5])(int, char*);           //原声明
typedef int* (*pFun)(int, char*);   //变量名为a,直接用一个新别名pFun替换a
pFun a[5];                          //原声明最简化版
 
void (*b[10]) (void (*)());         //原声明
typedef void (*pFunParam)();        //变量名为b,先替换右边部分括号里的,pFunParam为别名一
typedef void (*pFunx)(pFunParam);   //再替换左边的变量b,pFunx为别名二
pFunx b[10];                        //原声明最简化版
 
doube(*)() (*e)[9];                 //原声明
typedef double(*pFuny)();           //变量名为e,先替换左边部分,pFuny为别名一
typedef pFuny(*pFunParamy)[9];      //再替换右边的变量e,pFunParamy为别名二

定义结构体类型的详解:

理解复杂声明可用的“右左法则”:

从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完

1)int (*func)(int *p);

首先找到变量名func,外面有一对圆括号,而且左边是一个 * 号,这说明func是一个指针。然后跳出这个圆括号,先看右边,又遇到圆括号,这说明 ( * func)是一个函数;所以func是一个指向这类函数的指针,即函数指针;这类函数具有int*类型的形参,返回值类型是int。

2)int (*func[5])(int *);

func 右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个 * ,说明func的元素是指针(注意这里的* 不是修饰func,而是修饰 func[5]的,原因是[]运算符优先级比 * 高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。

2个模式:

typedef (*)(....) 函数指针

typedef (*)[] 数组指针

auto

27.关键字auto的作用是什么

关键字auto在C和C++编程语言中用于声明自动变量,它通常用于在函数内部声明变量,使得变量的存储期限与函数调用的生命周期相同。具体来说,使用auto关键字声明的变量会在函数调用结束后自动被销毁,不会占用内存空间。其实普通局部变量就是自动局部变量,只是省略了auto这一关键字。

在C++11标准中,auto关键字还被用于进行类型推断,可以根据变量的初始化表达式自动推断变量的类型,这种用法也被称为“自动类型推断”。例如:

auto i = 42;     // 推断 i 的类型为 int
auto d = 3.14;   // 推断 d 的类型为 double
auto s = "hello"; // 推断 s 的类型为 const char*

在这种情况下,编译器会根据初始化表达式的类型自动推断变量的类型,可以简化代码,提高可读性和可维护性。

register

28.关键字register的作用是什么?

在C和C++编程语言中,register关键字用于提示编译器将变量存储在CPU寄存器中,以便更快地访问它们。这种存储方式称为“寄存器变量”。

使用register关键字声明的变量不一定会被存储在寄存器中,因为编译器有权利忽略该提示。此外,只有局部变量和函数参数才可以声明为register变量,因为全局变量和静态变量不能存储在寄存器中。

在现代计算机上,编译器已经非常聪明,可以根据代码上下文自动地优化变量的存储和访问方式,因此register关键字的使用已经不再被推荐。事实上,许多编译器在遇到register关键字时会忽略它,因为它们已经可以自动进行寄存器分配和优化。

因此,在实际编程中,应该避免过度使用register关键字,而应该依赖编译器自动进行优化。如果确实需要强制要求将某个变量存储在寄存器中,可以使用特殊的语法和指令来实现,例如内嵌汇编。

注意:

  • register变量可能不存放在内存中,所以不能用&来获取该变量的地址;
  • 只有局部变量和形参可以作为register变量;
  • 寄存器数量有限,不能定义过多register变量。

attribute

29.请写个函数在main函数执行前先运行

attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性 (Type Attribute)。 gnu对于函数属性主要设置的关键字如下。

alias:设置函数别名。
aligned: 设置函数对齐方式。 
always_inline/gnu_inline: 函数是否是内联函数。
constructor/destructor: 主函数执行之前、之后执行的函数。
format: 指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置。 
noreturn:指定这个函数没有返回值。 请注意,这里的没有返回值,并不是返回值是void。而是像_exit/exit/abord那样执行完函数之后进程就结束的函数。
weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定的函数名称 命名有冲突,使用全局函数名称。

完整示例代码如下:

#include <stdio.h>
void before() __attribute__((constructor)); 
void after() __attribute__((destructor));
void before() 
{ 
    printf("this is function %s\n",__func__); 
 	return;
}
void after()
{ 
    printf("this is function %s\n",__func__); 
    return;
}
int main()
{ 
    printf("this is function %s\n",__func__); 
    return 0;
} 
// 输出结果
// this is function before 
// this is function main 
// this is function after

inline

30.关键字inline的作用

在C和C++语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。

为了解决这个问题,特别的引入了inline修饰符,表示为内联函数。

使用inline关键字声明的函数通常是较小的函数,因为将大型函数内联化可能会导致代码膨胀和性能下降。编译器会根据函数的实际情况决定是否内联函数,因此在实际编程中,程序员不应该过度依赖inline关键字,而应该让编译器自动进行优化。

在C++中,inline关键字还可以用于定义类的成员函数,这样可以在头文件中定义函数,避免多个源文件中引用同一个头文件时出现重复定义的错误。作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。

需要注意的是,inline关键字只是对编译器的一种提示,编译器有权利忽略这个提示。此外,在某些情况下,编译器可能会自动将函数内联化,而不需要使用inline关键字。因此,在实际编程中,应该根据具体情况考虑是否使用inline关键字。

31.内联函数与一般函数的区别

(1)内联含函数比一般函数在前面多一个inline修饰符

(2)内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,这与一般函数不同,主函数在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行

(3)一般函数的代码段只有一份,放在内存中的某个位置上,当程序调用它是,指令就跳转过来;当下一次程序调用它是,指令又跳转过来;而内联函数是程序中调用几次内联函数,内联函数的代码就会复制几份放在对应的位置上

(4)内联函数一般在头文件中定义,而一般函数在头文件中声明,在cpp中定义。

32.内联函数的优缺点和适用场景是什么

(1)优点:内联函数与宏定义一样会在原地展开,省去了函数调用开销,同时又能做类型检查。

(2)缺点:它会使程序的代码量增大,消耗更多内存空间。

内联函数适用场景:

  • 函数体内没有循环(执行时间短)且代码简短(占用内存空间小)。
  • 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
33.内联在哪个阶段展开?

内联函数是在编译时展开(编译器),而宏在预编译时展开(预处理器);在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

34.内联函数和宏定义的区别
  1. 内联函数是C++语言提供的一种特性,可以在函数定义时使用inline关键字进行声明。而宏定义是C和C++语言都支持的一种预处理指令
  2. 内联函数是由编译器实现的,因此内联函数的调用是有类型检查的。而宏定义是由预处理器实现的,宏定义的调用是没有类型检查的。这意味着在使用宏定义时,需要保证参数类型的正确性,否则会导致编译错误或者程序运行时错误。
  3. 内联函数的代码是由编译器直接嵌入到调用该函数的地方,因此内联函数的代码是可以进行调试的,可以在代码中打断点进行调试。而宏定义的代码是在预处理阶段直接替换为代码,无法进行调试
  4. 内联函数的参数和返回值都是有类型的,并且可以使用函数的所有特性(如const和引用参数)。而宏定义中的参数和返回值都是文本替换,不支持类型检查和特性
  5. 内联函数的使用受到编译器的限制,只有当函数体比较小、被频繁调用、参数是常量等条件满足时才会进行内联。而宏定义没有这些限制,它会在所有地方进行文本替换,因此可能会增加代码的体积和复杂度。
35.为什么不能把所有的函数写成内联函数?

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

  • 函数体内的代码比较长,将导致内存消耗代价
  • 函数体内有循环,函数执行时间要比函数调用开销大
36.构造函数、析构函数、虚函数可否声明为内联函数

首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。

举个例子:

#include <iostream>
using namespace std;
class A
{
public:
    inline A() {
		cout << "inline construct()" <<endl;
	}
    inline ~A() {
		cout << "inline destruct()" <<endl;
	}
    inline virtual void  virtualFun() {
		cout << "inline virtual function" <<endl;
	}
};
 
int main()
{
	A a;
	a.virtualFun();
    return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct() 

构造函数和析构函数声明为内联函数是没有意义,即编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。其次,class中的函数默认是inline型的,编译器也只是有选择性的inline,将构造函数和析构函数声明为内联函数是没有什么意义的。

将虚函数声明为inline,要分情况讨论

有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,而虚函数是运行期决定的,即在不知道将要调用哪个函数的情况下,如何将函数内联呢?

上述观点看似正确,其实不然,如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开。

综上,当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开;当是对象本身调用虚函数时,会内联展开,当然前提依然是函数并不复杂的情况下。

final和override

37.final和override关键字

override

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

class A
{
    virtual void foo();
}
class B : public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

如果不使用override,当你手一抖,将**foo()写成了f00()**会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

class A
{
    virtual void foo();
};
class B : public A
{
    virtual void f00(); //OK,这个函数是B新增的,不是继承的
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
    
};

mutable和explicit

38.mutable和explicit关键字的用法

mutable

mutable的中文意思是“可变的,易变的”,跟constant(C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后面关键字位置

class person
{
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
     void add() const//在函数里不可修改this指针指向的值 常量指针
     {
        m_A=10;//错误  不可修改值,this已经被修饰为常量指针
        m_B=20;//正确
     }
}

class person
{
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
}
int main()
{
    const person p;//修饰常对象 不可修改类成员的值
    p.m_A=10;//错误,被修饰了指针常量
    p.m_B=200;//正确,特殊变量,修饰了mutable
}

explicit

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换

注意以下几点:

  • explicit 关键字只能用于类内部的构造函数声明上
  • explicit 关键字作用于单个参数的构造函数
  • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换
39.隐式转换,如何消除隐式转换?

1、C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

2、C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。在比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。

3、基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象 子类对象可以隐式的转换为父类对象。

4、C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

5、如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

#C++##嵌入式##校招##面试##八股#
【嵌入式八股】一、语言篇 文章被收录于专栏

查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)

全部评论
这是我见过的最好的教程
点赞
送花
回复
分享
发布于 2023-03-28 14:21 辽宁
好牛!写的好!
点赞
送花
回复
分享
发布于 2023-03-29 11:21 河南
滴滴
校招火热招聘中
官网直投
写的好!
点赞
送花
回复
分享
发布于 2023-03-29 21:22 湖南
点赞
送花
回复
分享
发布于 2023-03-31 11:32 辽宁
点赞
送花
回复
分享
发布于 2023-04-02 09:58 辽宁
mark
点赞
送花
回复
分享
发布于 2023-04-21 14:52 重庆
MARK
点赞
送花
回复
分享
发布于 2023-08-12 10:06 河南
1
点赞
送花
回复
分享
发布于 2023-09-22 22:22 山东
m
点赞
送花
回复
分享
发布于 2023-10-04 14:34 江苏

相关推荐

13 32 评论
分享
牛客网
牛客企业服务