文章

从C++面试题来学习C++基础

花时间学习了下W3Cschool的C++基础 、转载至C++基础题汇总interview并结合自己的认识

struct和class的区别

  1. struct中的默认成员属性是public的;class中的默认成员属性是private的。struct是数据聚集起来,便于人访问,所以默认的是 public,而class是封装,不让人访问,所以是private。

  2. struct中不能有函数,但是可以通过使用函数指针(成员)的方式来实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    struct _TestS {
        void (*fun)();
    } TestS;
       
    void display() {
    }
       
    int main() {
        TestS.fun = display;
        return 0;
    }
    
  3. c struct不能有static成语属性,c++可以

    1
    2
    3
    4
    
    struct _TestS {
        int a;
        static int b;// error: 此处不能指定存储类
    } TestS;
    

结构体,类占内存大小

数据占用内存的大小取决于数据本身的大小其字节对齐方式

一般计算规则:

  1. 计算偏移量从0开始计算;
  2. 当前元素的偏移从元素自身大小的整数倍开始计算。
  3. 最后结果一定是结构体中最大基本数据类型的整数倍;

两大原则

第一原则:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为 0 计算)

第二原则:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,如果是,则结束;若不是,则补齐为它的整数倍。

struct 占内存大小

基础数据类型

数据类型32位64位取值范围(32位)
char11-128~127
unsigned char(当byte使用)110~255
short int / short22–32,768~32,767
unsigned short220~65,535
int44-2,147,483,648~2,147,483,647
unsigned int440~4,294,967,295
long int / long48–2,147,483,648~2,147,483,647
unsigned long480~4,294,967,295
long long int / long long88-9,223,372,036,854,775,808~9,223,372,036,854,775,807
指针48 
float443.4E +/- 38 (7 digits)
double881.7E +/- 308 (15 digits)

struct

空struct = 0 or 1

在C++语言中的确规定了空结构体和空类所占内存大小为1,而C语言中空类和空结构体占用的大小是0。C++中的空类与空结构体大小

C++语言标准中规定了这样一个原则:“no object shall have the same address in memory as any other variable”,即任何不同的对象不能拥有相同的内存地址。如果空类对象大小为0,那么此类数组中的各个对象的地址将会一致,明显违反了此原则。

C++编译器会在空类或空结构体中增加一个虚设的字节(有的编译器可能不止一个),以确保不同的对象都具有不同的地址。

class

函数不占,虚函数才占内寸,32位的占4位,64位占8位

函数不占内存

1
2
3
4
5
6
7
8
9
10
11
class Point {
    void test() {
        cout << "test" << endl;
    }
    void test2();
};

int main() {
    cout << sizeof(Point) << endl;// 1, 空类的大小
    return 0;
}

虚函数占,用来存放虚函数表的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
    virtual void test();
    virtual void test2() {
        cout << "teset" << endl;
    }
};

void Point::test() {
}

int main() {
    cout << sizeof(Point) << endl;// 8, 多个虚函数大小都是8,一个指针的大小
    return 0;
}

static = 0

c++中,因为static是存储在静态内存,而结构体是存储在栈上的。

1
2
3
4
5
6
7
8
class Point {
    static int a;
};

int main() {
    cout << sizeof(Point) << endl;// 1,空类的大小
    return 0;
}

union

联合类型的大小是最长的分量的长度,加上补齐的字节。这里容易有一个谬误,有人说补齐的字节是将联合类型的长度补齐为各分量基本类型的倍数,这个说法在默认的字节对齐(4字节或8字节)中没问题,但是当修改对齐方式之后就有问题了。先看一下默认的情况。

1
2
3
4
5
6
7
8
9
10
union U {
    char buff[17];
    int i;
    short k;
} u;

int main() {
    cout << sizeof(u) << endl; // 20, 最长属性位17,补全到默认的字节对齐(4字节的倍数)
    return 0;
}

使用#pragma pack(2),将默认对齐改成2

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(push)
#pragma pack(2)
union U {
    char buff[17];
    int i;
    short k;
} u;

int main() {
    cout << sizeof(u) << endl;// 18, 此时得到的最长分量还是13字节,不过扩充时不是按照4字节的倍数来算,而是按照2的倍数(pragma pack指定的)来算。最终得到大小为18字节。
    return 0;
}
#pragma pack(pop)

enum = 4

1
2
3
4
5
6
7
8
9
10
11
class Point {
    enum { red,
           green,
           blue,
           yellow } color;
};

int main() {
    cout << sizeof(Point) << endl;//4
    return 0;
}

#paragma pack(N)

表示按照N个字节对齐,如果N==1,则按照大小为所有类型所占字节的和,不用对齐,此时作用跟__attribute__((packed))一样,紧凑内存。

关于结构体占用空间大小总结

const

作用

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
  3. 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。

const的指针和引用

  • 指针
    • 指向常量的指针(pointer to const)
    • 自身是常量的指针(常量指针,const pointer)
  • 引用
    • 指向常量的引用(reference to const)
    • 没有 const reference,因为引用本身就是 const pointer

(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的 p2p3

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <pthread.h>

#include <cstdlib>
#include <iostream>

using namespace std;

// 类
class A {
   private:
    const int a;  // 常对象成员,只能在初始化列表赋值 ①
		int b;	
  
   public:
    int c;
    // 构造函数
    A() : a(0){};
    A(int x) : a(x){};  // 初始化列表 https://www.w3cschool.cn/cpp/cpp-constructor-destructor.html

    // const可用于对重载函数的区分
    int getValue();  // 普通成员函数
    int getValue() const;  // ② 常成员函数,不得修改类中的任何数据成员的值

    A(int x, int y);
};

A::A(int x, int y) { // ① error: "A::A(int x, int y)" 未提供初始值设定项: -- 常量 成员 "A::a"
   
}

int A::getValue() {
   a = 10;// ① error:常对象成员,只能在初始化列表赋值 
   return a;
}

int A::getValue() const {
   b = 10; // ② error:表达式必须是可修改的左值
   return a;
}

void function() {
    // 对象
    A b;  // 普通对象,可以调用全部成员函数、更新常成员变量
    const A a;        // ③常对象,只能调用常成员函数
    const A* p = &a;  // 指针变量,指向常对象
    const A& q = a;   // ④ 指向常对象的引用
		A& qt = a; // ④ error: 常对象,只能调用常成员函数;将 "A &" 类型的引用绑定到 "const A" 类型的初始值设定项时,限定符被丢弃
    a.c = 10; // ③ error:表达式必须是可修改的左值
    // 指针
    char greeting[] = "Hello";
    char greeting2[] = "Hello2";
    char* p1 = greeting;  // 指针变量,指向字符数组变量
    p1[0] = 'c';
    const char* p2 = greeting;  // 指针变量,指向字符数组常量(const 后面是
                                // char,说明指向的字符(char)不可改变)
    p2[0] = 'a'; // error: 表达式必须是可修改的左值
    char* const p3 = greeting;  // 自身是常量的指针,指向字符数组变量(const
                                // 后面是 p3,说明 p3 指针自身不可改变)
    p3 = greeting2; // error: 表达式必须是可修改的左值
    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();

int main() {
    A* atest = new A(22);
    return 0;
}

static

编译器在栈上分配空间

作用

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,而不是堆栈区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。生成对象也可以访问该函数

使用

  1. 被 static 修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要 new 出一个类来
  2. 被 static 修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要 new 出一个类来
  3. 静态成员可以通过双冒号来使用即 <类名>::<静态成员名>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Point {
   public:
    void init() {}
    static void output() {
        printf("%d\n", m_x);// error: 非静态成员引用必须与特定对象相对
    }

   private:
    static int m_x;
};
void main() {
    Point pt;
    pt.init();
    pt.output();
    Point::init();// error: 非静态成员引用必须与特定对象相对
    Point::output();
  
    static int count = 10;
    //这里的赋值实际上从未被执行过,这个count的初始化在程序装载的时候被执行
    //它的值被放在了全局变量区
    //这个过程发生在main函数被执行之前(和全局变量的初始化类似,在所有程序执行之前已经完成了初始化)
}

C++中的static关键字的总结

  1. 变量和全局变量

    当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化0 ‘\0’ NULL

访问修饰符不写默认为private

宏的介绍C++ 预处理器

  • static 关键字有哪些用法
  • 说说继承和组合的概念?什么时候应该用继承?什么时候应该用组合
  • C++ 的菱形继承会发生什么问题?画出对应的内存布局
  • 说说对 C++ 智能指针的了解
  • 说说虚函数实现机制
  • 如果父类中仅有方法,子类有一个 int 变量,这时候 sizeof 是多大
  • 指针一定是 4 个字节吗
  • #define 和 inline 函数的区别是什么
  • const static 在哪里初始化
  • 派生类的构造函数和析构函数执行顺序
  • 什么情况下基类的析构函数没有被调用
  • 如何生成静态库?如何生成动态库
  • 如何用 gdb 调试
  • coredump 查看,core 文件分析
  • 如何调试运行中的程序
  • 运行了几天的程序崩掉,如何分析错误
  • 如何编写 makefile
  • 编译器的编译过程?链接的时候做了什么事?在中间层优化时怎么做?
  • STL 各容器如何实现
  • 适配器是用来做什么的
  • queue 如何实现
  • map 用什么实现
  • 如何实现 vector?优化 O(n) 的复制
  • 红黑树插入数据发现不平衡应该怎么做
  • debug 模式和 release 模式编译的区别
  • 如何查看链接的动态库
本文由作者按照 CC BY 4.0 进行授权