Job Interview Notes: C++
[ C++ , interview ]

到了找工作的时节,自己却还不会c++,那怎么能找到好工作呢?于是我试着学习一些c++。

学习笔记没有很强的逻辑性,而且有很强的个人属性。学到哪里写哪里,哪里不会记哪里。学习笔记本来应该针对现代c++,但是由于本科时候学c++已经是很久以前,学了之后又从来没有正经使用过,因此笔记里会有很多很基础的c++语法或面向对象编程的相关内容和相对高级的c++特性夹杂在一起。

  1. 由leetcode简单题27. 移除元素 - 力扣(LeetCode)想到的
    1. 参数使用引用传递容易理解:要修改原本的vector
    2. vector在不使用引用传递时的行为是什么?

      值传递,拷贝一份,开销大,因为是深拷贝(underlying memory也会拷贝,而不只是拷贝一份指针)

    3. 这个行为是谁来做的?是编译器还是类声明的拷贝构造函数?

      不是编译器做的,是vector类自己声明的拷贝构造函数。

  2. 类的virtual function
    1. virtual function的调用是通过虚函数表来进行的(动态绑定,运行时多态)
    2. 每一个类对象的内存布局最前方保存了一个vptr,指向这个对象的虚函数表,虚函数表中再指向了具体的函数实现,虚函数在调用时,只与对象的实际类型有关,而与当前指针的静态类型无关
    3. 普通函数的调用是编译器直接在编译时决定了调用地址,callq 0x1210就可调用了,而不需要在运行时计算调用的地址。类的普通成员函数实质上是类的作用域中的全局函数,也就是和一个普通的函数没有区别,都是按照绝对的内存地址来调用的。类的内存布局里实际上只包含vptr和成员变量,而不包含普通成员函数。
    4. code example

       #include <iostream>
      
       class A {
       public:
           virtual void foo(void) {
               std::cout<<"A foo"<<std::endl;
           }
      
           void bar(void) {
               std::cout<<"A bar"<<std::endl;
           }
       };
      
       class B : public A {
           void foo(void) override {
               std::cout<<"B foo"<<std::endl;
           }
      
           void bar(void) {
               std::cout<<"B bar"<<std::endl;
           }
       };
      
       int main() {
           B b;
           A* a = &b;
           a->foo(); // B foo
           a->bar(); // A bar
           return 0;
       }
      
  3. push_back() vs emplace_back()
    1. push_back()的参数如果是一个临时变量,会调用拷贝构造函数在末尾添加,如果是一个右值对象(MyClass()),会调用移动构造函数在末尾添加
    2. emplace_back()的参数如果是构造函数的参数如vec.emplace_back()(空构造函数),会直接在末尾调用构造函数来添加对象
    3. emplace_back()的参数如果是临时变量或者右值,则没有性能差异。
  4. Plain Old Data(POD)
    1. POD服从C ABI,可以进行二进制传递
    2. POD = trivial && standard_data_layout
    3. is_pod_v(obj)(deprecated)=is_standard_data_layout_v(obj) && is_trivial_v(obj)
    4. Trivial: 所有的构造、析构、移动、拷贝、赋值都是由编译器自动生成的,可以通过简单的memcpy来复制,memmove来移动
    5. Standard-layout:符合C语言中struct的标准内存排列
      1. 所有非静态成员都是相同的访问控制(public,private,protected)
      2. 没有虚函数或虚基类
      3. 所有非静态数据成员在基类中的顺序与其声明顺序一致。
      4. 不能有多个基类中包含相同类型的成员
  5. 虚基类(virtual base)
    1. 解决菱形继承问题
  6. Data member pointer
    1. data member pointer(数据成员指针)是指向类的成员变量(非静态成员)的指针。它允许通过指针来访问类的某个成员变量,而不是通过对象直接访问。
    2. 为什么不能是静态成员?data member pointer是一个相对于对象的内存布局的offset,而静态成员位于单独的内存区域,不和任何一个对象有关系。
    3. code example

       #include <iostream>
       using namespace std;
          
       class X {
       public:
         int a;
         void f(int b) {
           cout << "The value of b is "<< b << endl;
         }
       };
          
       int main() {
          
         // declare pointer to data member
         int X::*ptiptr = &X::a;
          
         // declare a pointer to member function
         void (X::* ptfptr) (int) = &X::f;
          
         // create an object of class type X
         X xobject;
          
         // initialize data member
         xobject.*ptiptr = 10;
          
         cout << "The value of a is " << xobject.*ptiptr << endl;
          
         // call member function
         (xobject.*ptfptr) (20);
       }
      
  7. C++11不再允许将字符串字面量赋值给一个char*,而只能赋值给一个const char*
    1. 原因: 字符串字面量本来就存储在不可写的内存区域,而char*是可写的,如果将字符串字面量赋值给char*,可能会破坏这个不可写的内存区域,导致未定义行为。
    2. 如果需要一个可写的字符串,可以使用std::string = "hello"或者char str[] = "hello"std::string实际上是通过重载=操作符来实现的将.rodata段上的字符串拷贝到堆上,而char str[]实际上是编译器进行的栈上内存分配和拷贝。
  8. const相关
    1. const变量真的不能修改吗?考虑code example:

       const int global_a = 10;
       int main() {
         const int local_a = 10;
         return 0;
       }
      

      使用lldb查看global_alocal_a的memory region,结果如下:

       (lldb) memory region &global_a
       [0x0000000100000000-0x0000000100004000) r-x __TEXT
       Modified memory (dirty) page list provided, 1 entries.
       Dirty pages: 0x100000000.
       (lldb) memory region &local_a
       [0x000000016f604000-0x000000016fe00000) rw-
       Modified memory (dirty) page list provided, 6 entries.
       Dirty pages: 0x16fde8000, 0x16fdec000, 0x16fdf0000, 0x16fdf4000, 0x16fdf8000, 0x16fdfc000.
      

      事实上只有global_a在内存中是只读的,local_a在内存中是可写的。const变量只是告诉编译器这个变量是只读的,但是并不会真的将这个变量放在只读内存区域。也就是说,const变量不是真的不能改,而是编译器会拒绝这个代码。

    2. 我真的想改const变量!
      1. 最简单的想法:既然内存是可以改的,那我拿到内存地址,直接改就行了!
         const int global_a = 10;
         int main() {
           const int local_a = 10;
           int *local_a_ptr = &local_a;
           *local_a_ptr = 20;
           return 0;
         }
        
        

      事实是,编译器仍然会拒绝这样的代码。

       test.cpp:4:8: error: cannot initialize a variable of type 'int *' with an rvalue of type 'const int *'
           4 |   int *local_a_ptr = &local_a;
             |        ^             ~~~~~~~~
       1 error generated.
      

      编译器为什么会拒绝这样的代码呢?因为指针类型和指向的类型需要匹配,我们只能用const int *来指向const int,而不能用int *来指向const int。否则const的意义也太弱了。

      1. 如果硬要改,其实还是很简单的,但是会出现一些预期外的行为。

         #include <iostream>
         int main() {
           const int a = 10;
           // NOT ALLOWED by the compiler
           // int *a_ptr = static_cast<int *>(&a);
           int *a_ptr = (int *)&a;
           *a_ptr = 11;
           std::cout << a << " " << *a_ptr << std::endl;
           std::cout << &a << " " << a_ptr << std::endl;
           return 0;
         }
        

      上面的代码输出:

       10 11
       0x16d8ff428 0x16d8ff428
      

      可以看到,a输出的值没有改变,但是a_ptr指向的值改变了,但他们本来应该是同一个值。因为编译器对声明为const的变量做了优化,默认在运行时不会改变,自然不需要再从内存访问。通过指针这样改变const变量的值,这样的行为是未定义的,不应该这样做。当然,也可以把a声明为volatile const int,这样起码输出的值是对的。C++还提供了const_cast来去掉const属性,但是int *a_ptr = const_cast<int *>(&a);这样的代码也是未定义行为。

    3. const和指针/引用。C++中指针/引用类型和指向/引用的类型需要匹配,但是const是一个例外,例如:

       int a = 10;
       const int &a_ref = a; // a_ref is a reference to const int but a is non-const int
       const int *a_ptr = &a; // a_ptr is a pointer to const int but a is non-const int
       a = 11; // OK
       // a_ref = 12; // NOT ALLOWED by the compiler
       // *a_ptr = 13; // NOT ALLOWED by the compiler
      
    4. const pointer VS pointer to const
      1. const pointer: int *const a_ptr = &a;a_ptr是一个指向int的常量指针,指针本身是常量,指向的对象不是常量,a_ptr不能指向其他对象,但是可以改变指向对象的值。
      2. pointer to const: const int *a_ptr = &a;a_ptr是一个指向const int的指针,指针本身不是常量,指向的对象是常量,a_ptr可以指向其他对象,但是不能改变指向对象的值。
      3. 技巧:从右到左读。int *const里面,int是base type,*const是type declarator。const int *里面,const int是base type,*是type declarator。

first published: 2024-09-14 16:06:28 CST
last modified: 2024-10-25 01:11:15 CST

revision history