C/C++面试基础知识总结(一)

1.const

1.1 作用

  • 修饰变量,说明该变量不可以被改变;
  • 修饰指针,分为指向常量的指针和指针常量;
  • 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  • 修饰成员函数,说明该成员函数内不能修改成员变量。

const 定义的变量只有类型为整数或枚举,且以常量表达式初始化时才能作为常量表达式。其他情况下它只是一个 const 限定的变量,不要将与常量混淆。

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

1.2 指针常量和常量指针的区别

  • 1.指针常量与常量指针的概念:指针常量就是指针本身是常量,换句话说,就是指针里面所存储的内容(内存地址)是常量,不能改变。但是,内存地址所对应的内容是可以通过指针改变的。
    常量指针就是指向常量的指针,换句话说,就是指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
  • 2.指针常量与常量指针的声明:指针常量的声明 数 据 类 型 ∗ c o n s t 指 针 变 量 数据类型 * const 指针变量 const
    常量指针的声明: 数 据 类 型 c o n s t ∗ 指 针 变 量 或 者 c o n s t 数 据 类 型 ∗ 指 针 变 量 数据类型 const * 指针变量 或者 const 数据类型 *指针变量 constconst
    常量指针常量的声明: 数 据 类 型 c o n s t ∗ c o n s t 指 针 变 量 或 者 c o n s t 数 据 类 型 ∗ c o n s t 指 针 变 量 数据类型 const * const 指针变量 或者 const 数据类型 * const 指针变量 constconstconstconst
  • 3.指针常量的例子
/*指针常量的例子*/ 
int a,b; 
int * const p; 
p = &a;//正确 
p = &b;//错误 
*p = 20;//正确 

指针常量声明的时候必须赋初始值。使用指针常量可以增加代码的可靠性和执行效率。

  • 4.常量指针的例子
/*常量指针的例子*/ 
int a,b; 
int const *p; 
p = &a;//正确 
p = &b;//正确 
*p = 20;//错误 

关于区分指针常量的一个小技巧:const后的内容为不能修改的。例如指针常量 int * const p = &a;则表示指针p的内容不能修改;常量指针int const *p = &a;则表示指针p所指向的内容不能修改。

1.3 const对象默认为文件局部变量

注意非const变量默认为extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。

// 未被const修饰的变量在不同文件的访问
// file1.cpp
int ext
// file2.cpp
#include<iostream>
/**
 * compile: g++ -o file file2.cpp file1.cpp
 * execute: ./file
 */
extern int ext;
int main(){
    std::cout<<(ext+10)<<std::endl;
}

//const常量在不同文件的访问
//extern_file1.cpp
extern const int ext=12;
//extern_file2.cpp
#include<iostream>
/**
 * compile: g++ -o file const_file2.cpp const_file1.cpp
 * execute: ./file
 */
extern const int ext;
int main(){
    std::cout<<ext<<std::endl;
}

小结:可以发现未被const修饰的变量不需要extern显式声明!而const常量需要显式声明extern,并且需要做初始化!因为常量在定义后就不能被修改,所以定义时必须初始化。

1.4 类中使用const

在一个类中,任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。使用const关键字进行说明的成员函数,称为常成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字声明的成员函数不能用来操作常对象。

对于类中的const成员变量必须通过初始化列表进行初始化,如下所示:

class Apple
{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
};
Apple::Apple(int i):apple_number(i)
{
}

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.

1.5 代码使用

// 类
class A
{
private:
    const int a;                // 常对象成员,只能在初始化列表赋值
public:
    // 构造函数
    A() { };
    A(int x) : a(x) { };        // 初始化列表
    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数
    const A a;                  // 常对象,只能调用常成员函数、更新常成员变量
    const A *p = &a;            // 常指针
    const A &q = a;             // 常引用
    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 常量指针,指向字符数组常量
    char* const p3 = greeting;          // 指针常量,指向字符数组变量
    const char* const p4 = greeting;    // 常指针常量,指向字符数组常量
}
// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数指针所指内容为常量
void function3(char* const Var);         // 参数指针为常指针
void function4(const int& Var);          // 引用参数在函数内为常量
// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

对于非内部数据类型的参数而言,像void func(A a) 这样声明的函数注定效率比较低。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。

为了提高效率,可以将函数声明改为void func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void func(A &a) 存在一个缺点:

“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void func(const A &a)。

以此类推,是否应将void func(int x) 改写为void func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。

小结:对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void func(A a) 改为void func(const A &a)。对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void func(int x) 不应该改为void func(const int &x)。

2.static

2.1 修饰普通变量

修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。

#include <iostream> 
#include <string> 
using namespace std; 
void demo() 
{ 
    // static variable 
    static int count = 0; 
    cout << count << " "; 
    // 值被更新,并将被传递到下一次函数调用
    count++; 
} 
int main() 
{ 
    for (int i=0; i<5; i++)  
        demo(); 
    return 0; 
} 
//输出:0 1 2 3 4 

您可以在上面的程序中看到变量count被声明为static。因此,它的值通过函数调用来传递。每次调用函数时,都不会对变量计数进行初始化。

2.2 修饰普通函数

修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命令函数重名,可以将函数定位为 static。

2.3 修饰成员变量

修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
由于声明为static的变量只被初始化一次,因为它们在单独的静态存储中分配了空间,因此类中的静态变量由对象共享。对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使用构造函数初始化。

#include<iostream> 
using namespace std; 
class Apple 
{ 
public: 
    static int i; 
    Apple() 
    { 
        // Do nothing 
    }; 
}; 
int main() 
{ 
Apple obj1; 
Apple obj2; 
obj1.i =2; 
obj2.i = 3; 
// prints value of i 
cout << obj1.i<<" "<<obj2.i; 
// 输出:3 3
} 

您可以在上面的程序中看到我们已经尝试为多个对象创建静态变量i的多个副本。但这并没有发生。因此,类中的静态变量应由用户使用类外的类名和范围解析运算符显式初始化,如下所示:

#include<iostream> 
using namespace std; 
class Apple 
{ 
public: 
    static int i; 
    Apple() 
    { 
        // Do nothing 
    }; 
}; 
int Apple::i = 1; 
int main() 
{ 
    Apple obj; 
    // prints value of i 
    cout << obj.i; 
} 
// 输出:1

2.4 修饰成员函数

修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
就像类中的静态数据成员或静态变量一样,静态成员函数也不依赖于类的对象。我们被允许使用对象和’.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员。
允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。

#include<iostream> 
using namespace std; 
class Apple 
{ 
    public: 
        // static member function 
        static void printMsg() 
        {
            cout<<"Welcome to Apple!"; 
        }
}; 
// main function 
int main() 
{ 
    // invoking a static member function 
    Apple::printMsg(); 
   // 输出:Welcome to Apple!
} 

3.this

3.1 this 指针

  • this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向正在被该成员函数操作的那个对象。
  • 当对一个对象调用成员函数时,编译程序先将对象的地址赋给this 指针,然后调用成员函数,每次成员函数存取数据成员时,由隐含使用 this 指针。
  • 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  • this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
  • this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  • 在以下场景中,经常需要显式引用 this 指针:
    • 为实现对象的链式引用;
    • 为避免对同一对象进行赋值操作;
    • 在实现一些数据结构时,如 list

3.2 this指针的用处

  • 1.一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
  • 2.this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
  • 3.在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this
  • 4.当参数与成员变量名相同时,如this->n = n (不能写成n = n)。

4. inline 内联函数

4.1 特征

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 不能包含循环、递归、switch 等复杂操作;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

4.2 类中内联

// inline.h
// 头文件中声明方法
class A
{
public:
    void f1(int x); 
    /**
     * @ 类中定义了的函数是隐式内联函数;声明后,要想成为内联函数,必须在实现处(定义处)加inline关键字。
     */
    void Foo1(int x,int y) ///< 定义即隐式内联函数!
    {
    };
    void f1(int x); ///< 声明后,要想成为内联函数,必须在定义处加inline关键字。  
};

实现文件中定义内联函数

#include <iostream>
#include "inline.h"
using namespace std;
/*
 inline要起作用,inline要与函数定义放在一起,inline是一种“用于实现的关键字,而不是用于声明的关键字”
 */
int Foo(int x,int y);  // 函数声明
inline int Foo(int x,int y) // 函数定义
{
    return x+y;
}
// 定义处加inline关键字,推荐这种写法!
inline void A::f1(int x){
}
int main()
{
    cout<<Foo(1,2)<<endl;
}
/**
 * 编译器对 inline 函数的处理步骤
 * 将 inline 函数体复制到 inline 函数调用点处;
 * 为所用 inline 函数中的局部变量分配内存空间;
 * 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
 * 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
 */

内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

  • 如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
  • 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下情况不宜用内联:

(1)如果函数体内的代码比较长,使得内联将导致内存消耗代价比较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

4.3虚函数(virtual)可以是内联函数(inline)吗?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
#include <iostream>  
using namespace std;
class Base
{
public:
    inline virtual void who()
    {
        cout << "I am Base\n";
    }
    virtual ~Base() {}
};
class Derived : public Base
{
public:
    inline void who()  // 不写inline时隐式内联
    {
        cout << "I am Derived\n";
    }
};
int main()
{
    // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
    Base b;
    b.who();
    // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
    Base *ptr = new Derived();
    ptr->who();
    // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
    delete ptr;
    ptr = nullptr;
    system("pause");
    return 0;
}

5.assert()

断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前调用。

#include <stdio.h> 
#include <assert.h> 
int main() 
{ 
    int x = 7; 
    /*  中间有一些代码,假设 x 意外更改为 9  */
    x = 9; 
    // 程序员在其余代码中假设 x 为 7
    assert(x==7); 
    /* 代码的其余部分 */
    return 0; 
} 

断言主要用于检查逻辑上不可能的情况。例如,它们可用于检查代码在开始运行之前所期望的状态,或者在运行完成后检查状态。与正常的错误处理不同,断言通常在运行时被禁用。
忽略断言:

#define NDEBUG          // 加上这行,则 assert 不可用
#include <assert.h>
assert( p != NULL );    // assert 不可用

6.sizeof()

6.1特征

  • 空类的大小为1字节
  • 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间。
  • 对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
  • 普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小
  • 虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!
  • 虚继承,继承基类的vptr。

6.2 原则1

/**
 * @file blackclass.cpp
 * @brief 空类的大小为1字节
 */
#include<iostream>
using namespace std;
class A{};
int main()
{
    cout<<sizeof(A)<<endl;
    return 0;
}

6.3 原则2

/**
 * @file static.cpp
 * @brief 静态数据成员
 * 静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员,但不影响类的大小。不管这个类产生了多少个实例,还是派生了多少新的类,静态数据成员只有一个实例。静态数据成员,一旦被声明,就已经存在。 
 */
#include<iostream>
using namespace std;
class A
{
    public:
        char b;
        virtual void fun() {};
        static int c;
        static int d;
        static int f;
};
int main()
{
    /**
     * 16  字节对齐、静态变量不影响类的大小、vptr指针=8
     */
    cout<<sizeof(A)<<endl; 
    return 0;
}

6.3 原则3

/**
 * @file morevir.cpp
 * @brief 对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
 */
#include<iostream>
using namespace std;
class A{
    virtual void fun();
    virtual void fun1();
    virtual void fun2();
    virtual void fun3();
};
int main()
{
    cout<<sizeof(A)<<endl; // 8
    return 0;
}

6.4 原则4和5

/**
 * @file geninhe.cpp
 * @brief 1.普通单继承,继承就是基类+派生类自身的大小(注意字节对齐)
 * 注意:类的数据成员按其声明顺序加入内存,无访问权限无关,只看声明顺序。
 * 2.虚单继承,派生类继承基类vptr
 */
#include<iostream>
using namespace std;
class A
{
    public:
        char a;
        int b;
};
/**
 * @brief 此时B按照顺序:
 * char a
 * int b
 * short a
 * long b
 * 根据字节对齐4+4=8+8+8=24
 */
class B:A
{
    public:
        short a;
        long b;
};
class C
{
    A a;
    char c;
};
class A1
{
    virtual void fun(){}
};
class C1:public A
{
};
int main()
{
    cout<<sizeof(A)<<endl; // 8
    cout<<sizeof(B)<<endl; // 24
    cout<<sizeof(C)<<endl; // 12
    /**
     * @brief 对于虚单函数继承,派生类也继承了基类的vptr,所以是8字节
     */
    cout<<sizeof(C1)<<endl; // 8 
    return 0;
}

6.5原则6

/**
 * @file virnhe.cpp
 * @brief 虚继承
 */
#include<iostream>
using namespace std;
class A
{
    virtual void fun() {}
};
class B
{
    virtual void fun2() {}
};
class C : virtual public  A, virtual public B
{
    public:
        virtual void fun3() {}
};
int main()
{
    /**
     * @brief 8 8 16  派生类虚继承多个虚函数,会继承所有虚函数的vptr
     */
    cout<<sizeof(A)<<" "<<sizeof(B)<<" "<<sizeof(C);
    return 0;
}

7.#pragma pack(n)

设定结构体、联合以及类成员变量以 n 字节方式对齐:

#pragma pack(push)  // 保存对齐状态
#pragma pack(4)     // 设定为 4 字节对齐
struct test
{
    char m1;
    double m4;
    int m3;
};
#pragma pack(pop)   // 恢复对齐状态

8.volatile

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:volatile int vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

一般说来,volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile
  2. 多任务环境下各任务间共享的标志应该加volatile
  3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

参考目录

https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777432.html
https://www.bookstack.cn/read/CPlusPlusThings/ec48d16ca2a6afb0.md

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值