C++知识点小结

C++基础知识(第一部分:定义为主)


C++必备知识点

C++中指针常量和常量指针的区别

[ 参考链接 ]

指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。

  • 常量指针

    又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。

  • 指针常量

    本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以是只读的,指向的地址不可以变化,但是指向的地址所对应的内容可以变化 。

  • 如何使用cout打印char *指针所指向的地址

    如果给cout提供一个指针,它将打印指针所指向的地址单元的地址,但如果指针类型为char ,则cout将打印char 指针所指向的字符串。如果要显示char 指针所指向的地址单元的地址,需要将char 类型的指针强制转化为另一种类型的指针,我将char 类型的指针强制转化为int 类型指针。

  • 一个递归中使用指针出现的错误

    下面代码的基本逻辑是在递归中序遍历过程中判断序列是否为升序。代码中并没有使用全局变量pre,而是将pre作为函数的参数,此时本质上相当于按照“值”传递参数,即传递的是一个地址。本来,预期的结果是,在递归遍历到第一个叶子节点后会返回,然后pre被赋值为这个叶子节点。但是,当递归调用返回上一层父节点时,pre的值仍然为nullptr,并没有被修改为左叶子的地址。

    实际上递归调用的时候会生成"栈帧"保存当前调用的pre、curr指针的值,所以后续递归中改变pre的值并不会对之前的“栈帧“中的pre造成影响。今后遇到这类递归,应当改写成非递归形式,或者使用全局变量。(指针是按值传参)

class Solution {
 public:
  bool res = true;
  bool isValidBST(TreeNode *root) {
    inorderTraversal(root, nullptr);
    return res;
  }

  void inorderTraversal(TreeNode *curr, TreeNode *pre) {
    if (curr == nullptr) return;
    inorderTraversal(curr->left, pre);
    if (pre != nullptr && pre->val >= curr->val) {
      res = false;
    }
    cout << curr->val << " " << (pre ? pre->val : 0) << endl;
    pre = curr;
    inorderTraversal(curr->right, pre);
  }
};

C语言中各种类型的全局变量默认值

[ C语言中各种类型的全局变量默认值 ]

int: 0、char: '\0'也就是字符结束的标记、float: 0、double:0、string:默认是空字符串也就是""或者NULL、bool: 0、int* 0、float* 0 double* 0、string* 0、char*:针类型除了char*默认是空字符串''或者NULL,其他就默认都是0。

char *a = "abc";
char b[3] = { 'a', 'b' };
b[0] = 'b';
a[0] = 'b';

char*a = ”abc“ 的 ”abc“ 是常量所以被分配在常量存储区。a为字符指针类型是存放在栈区可以被更改,而常量存储区是不能更改的,是只读区域。所以当你试图去写入数据的时候,会报错。而char b[3] = {‘a’,‘b’}的字符a和b是存放在栈区的所以可以进行写操作。


char与signed char, unsigned char这三者的区别

  • 定义方式

    // 函数原型 int func(int a, int b);
    int (*pfun)(int, int);
    typedef int (*pf) (int, int);
    using PF = int (*)(int)(int);

    指针函数即返回指针的函数,首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量 。

     int (*f) (int x); /* 声明一个函数指针 */ f=func; /* 将func函数的首地址赋给指针f*/

  • C/C++规定函数名就表示函数入口地址,函数名赋值时函数名前面加不加取地址符&均可,也就是说PF f = func等价于PF f = &func

  • 回调函数(Callback function)

    函数指针本质是一个指针,只不过这个指针指向一个函数。 函数指针变量也是一个变量,那么作为变量当然也可以当做参数来使用。 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。 (对比之下,在函数过程中调用别的函数,可以称作调用)。在C语言中提供了快速排序的函数:void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ),qsort函数中包含一个函数指针compare,用于比较两个元素,该函数由用户传入,即回调函数。关于回调函数,也可以从调用方的角度考虑,平时写代码我们会调用系统提供的API,如果我们将自行实现的函数作为参数传递给系统去调用,那么可以被称作“回调”。

    A "callback" is any function that is called by another function which takes the first function as a parameter. (From StackOverflow)

  • 经典例子

    [ 易于理解的解释 ]:先看核心部分: signal(int, void (*)(int)),这就是signal函数名 加上 参数部分,而第二个参数是一个函数指针;将这一部分设为x,代入原型之后,其实就是返回值类型了:void (*x)(int);

    void (*signal(int sig, void (*func)(int)))(int);
    // 等价于
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int sig, sighandler_t func);

C++面向对象三大特征

[ c++继承详解之一——继承的三种方式、派生类的对象模型 ]

封装、继承、多态。

其中,继承分为:

  • 实现继承是指直接使用基类的属性和方法而无需额外编码的能力;
  • 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。

多态可以概括为“一个接口,多种方法”。分为两种:

  • 编译时多态性(静态多态):通过重载函数实现:先期联编 early binding

  • 运行时多态性(动态多态):通过虚函数实现 :滞后联编 late binding


重写、覆写、重载

#include <stdio.h>

#include <iostream>
class Parent {
 private:
  int a;

 public:
  Parent(int s = 10) : a(s) {}
  void F() { printf("Parent.F()/n"); }
  virtual void G() { printf("Parent.G()/n"); }
  int Add(int x, int y) { return x + y; }
  //重载(overload)Add函数
  float Add(float x, float y) { return x + y; }
  friend void show(const Parent& t);
};

class ChildOne : public Parent {
 public:
  //重写(overwrite)父类函数
  void F() { printf("ChildOne.F()\n"); }

  //覆写(override)父类虚函数,主要实现多态
  void G() { printf("ChildOne.G()\n"); }
};

void show(const Parent& t) { std::cout << t.a << std::endl; }

int main() {
  ChildOne childOne;  // = new ChildOne();
  Parent* p = &childOne;
  //调用ChildOne.F
  // childOne.F();
  show(*p);
  //调用Parent.F()
  p->F();
  //实现多态
  p->G();
  Parent* p2 = new Parent();
  //重载(overload)
  printf("%d/n", p2->Add(1, 2));
  printf("%f/n", p2->Add(3.4f, 4.5f));
  delete p2;
  system("PAUSE");
  return 0;
}

extern作用:声明 全局变量和全局函数

[ 参考 ]

函数的声明extern关键词是可有可无的,因为函数本身不加修饰的话就是extern的。但是引用的时候一样是需要声明的。

而全局变量在外部使用声明时(注意只有全局变量才能在外部使用),extern关键词是必须的。如果变量无extern修饰且没有显式的初始化,同样属于变量定义而非声明。因此,此时必须加extern,而编译器在此标记存储空间,在执行时加载入内存并初始化为0。而局部变量的声明不能有extern的修饰,且局部变量在运行时才在堆栈部分分配内存。

使用全局变量或函数的两种方式:

  • 在头文件中中显式使用extern声明全局变量或函数,然后在*.cpp文件下#include头文件调用;另外一种方式是需调用的源文件base.cpp中已经存在定义好的“全局变量和函数”(仅限base.cpp),则在当前源文件中调用需要使用extern显式声明。

  • extern "C" 和extern "C++"函数声明用于指明链接规范,如extern "C",就指明使用C语言的链接规范。

    适用场景:C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时无法找到对应函数的情况,此时C函数就需要用extern “C”进行链接指定。

    // 声明printf函数使用C链接
    extern "C" int printf(const char *fmt, ...);
    
    // 声明函数ShowChar和GetChar使用 C 链接
    extern "C" {
        char ShowChar(char ch);
        char GetChar(void);
    }

左值与右值

[ 参考左值引用、右值引用、移动语义、完美转发 ]

左值:指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。

右值:术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。

对于左值的引用就是左值引用,而对于右值的引用就是右值引用

规则简化如下:

左值引用 {左值}

右值引用 {右值}

常左值引用 {右值}

C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值(pvalue)、将亡值(xvalue,expiring value,即将被销毁、却能够被移动的值)

std::string str = "Hello";
std::vector<std::string> v;

v.push_back(str);
std::cout << "After copy, str is " << str << "\n";
//输出结果为 After copy, str is "Hello"

v.push_back(std::move(str));
std::cout << "After move, str is " << str << "\n";
//输出结果为 After move, str is ""

全局变量、局部变量、形式参数

局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。

当局部变量被定义时,系统不会对其初始化,定义全局变量时,系统会自动初始化。


常量与进制

二进制:0b+ 数字(0-1) ; 0b1010101

八进制:0+ 数字(0-7); 0125

十进制: 数字(0-9); 85

十六进制:0x+数字(0-9,A-F);0x55 #前缀可大写


volatile、static

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,例如多任务环境下各任务间共享的标志应该加 volatile

当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期,但未改变作用域; static修饰全局变量,并为改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量。用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。[ 原文链接 ]

staticconst是否可以联合使用?: 可以使用staticconst修饰成员;但是不能用于同时修饰函数,存在语义矛盾,const修饰的函数会为函数隐式添加参数this,并且将this指针指向的对象置为conststatic修饰的函数没有this指针,与const的用法冲突。


运算符优先级

[ C++ Operator Procedure关于后缀自增运算符优先级问题for-loop中使用++ii++的性能区别 ]

在"c=d++"中,后缀运算符的优先级更高的意义在于这个表达式应该被理解为"c=(d++)"而不是"(c=d)++"。而且,后一个表达式是不正确的,这就涉及到 lvalue 的问题了。后缀运算符"++"要求其修饰的表达式是 lvalue 的。所以后边一个表达式通常会无法编译。


输出控制符号

  • printf

    [ format ] (#include <cstdio>)

  • std::ostream::operator<<

    [ manipulators ] (#include <iomanip>)


C/C++中的数学运算支持与随机数

  • cmath支持的函数

    [ 参考文档 ]

  • 随机数字

    #include <iostream>
    #include <stdlib.h>
    #include <time.h> 
    
    using namespace std; 
    
    int main(){ 
      srand((unsigned)time(NULL)); // 不属于std命名空间
      for(int i = 0; i < 10;i++ ) 
        cout << rand() << '/t';
      cout << endl; 
      return 0;
    }

    要取得[a,b)的随机整数,使用(rand() % (b-a))+ a; 要取得[a,b]的随机整数,使用(rand() % (b-a+1))+ a; 要取得(a,b]的随机整数,使用(rand() % (b-a))+ a + 1; 通用公式:a + rand() % n;其中的a是起始值,n是整数的范围。 要取得a到b之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。 要取得0~1之间的浮点数,可以使用rand() / double(RAND_MAX)。


C++传递数组的三种方式

主要需要复习的知识点是数组名与数组指针。

void myFunction(int *param);
void myFunction(int param[10]);
void myFunction(int param[]);

cstring中的字符数组操作和C++的string类别

strcpy(t, s) strcat(t, s) strlen(s) strcmp(s1, s2) strchr(s1, ch) strstr(s1, s2)


指针(C和C++中的重要概念)

详见C++面向对象编程.md

  • 指针与引用的区别

    1. 引用必须在创建时被初始化;指针可以在任何时间被初始化。
    2. 引用被初始化为一个对象就不可改变;指针可以在任何时候指向到另一个对象。
    3. 不存在空引用。引用必须连接到一块合法的内存。

swap的注意点

在标准库中,swap的实现是基于引用的,可用于交换两个同类型变量的值。

template <class T>
void swap(T& a, T& b) {
  T c(std::move(a));
  a = std::move(b);
  b = std::move(c);
}
template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) {
  for (size_t i = 0; i < N; ++i) swap(a[i], b[i]);
}
  • 需要注意,交换迭代器指向的内容,应当使用iter_swap,等价于swap(*it1, *it2)
  • 亦可以使用swapstd::vector::swap交换两个vector的所有内容,容器长度可以不同。

auto与decltype自动类型推导

[ 如何评价 C++ 11 auto 关键字 ]


运算符重载

  • 不可重载的运算符

    .成员访问运算符 .*, ->*成员指针访问运算符 ::域运算符

    sizeof长度运算符 ? :条件运算符 #预处理符号

  • 可重载运算符

双目算术运算符 + (加),-(减),*(乘),/(除),% (取模)
关系运算符 ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于)
逻辑运算符 ||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符 + (正),-(负),*(指针),&(取地址)
自增自减运算符 ++(自增),--(自减)
位运算符 | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符 =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
空间申请与释放 new, delete, new[ ] , delete[]
其他运算符 ()(函数调用),->(成员访问),,(逗号),[] (下标)
  • 前缀自增/减运算符(++ unary-expression)、后缀自增/减运算符(postfix-expression ++)重载方式

    // Declare prefix and postfix increment operators.
    Point& operator++();    // Prefix increment operator.
    Point operator++(int);  // Postfix increment operator. 
    
    // Declare prefix and postfix decrement operators.
    Point& operator--();    // Prefix decrement operator.
    Point operator--(int);  // Postfix decrement operator.

map、set、priority_queue的自定义排序顺序

上述三类容器中的元素在STL中默认升序排列,需要注意此时priority_queue是大顶堆。优先队列默认是大根堆,即最大元素在数组开头,而使用的默认less比较方式是因为,在堆进行上浮或者下沉的操作中会使用less比较,判断若根节点元素小于其子节点,则返回true,进行调整,较大元素上浮。

==所以,less表现为大顶堆而greater表现为小顶堆。==

可以通过自定义一个比较类(仿函数类)并重载函数调用运算符(),例子如下:

template <typename Type>
struct CMP {
  bool operator() (const Type& lhs, const Type& rhs) const {return lhs < rhs;} // pq大顶堆,小的向下交换
};
priority_queue<Type, vector<Type>, CMP<Type>> pq;

第二种方式是重载关系运算符<实现对象的比较,例子如下:

struct Node {
  int val;
  bool operator<(const Node a) const {return val > a.val;} // pq小顶堆,大的向下交换
};
priority_queue<Node> pq;

而对于<algorithm>中的sort函数,则还可以自定义比较函数,例子如下:

bool cmp (const Node a, const node b) { return (a.val < b.val);} // 数组元素升序排列
sort(nodes.begin(), nodes.end(), cmp);

最后一种我用得比较少,使用lambda函数,例子如下:

auto cmp = [](const std::pair<int, int> &l, const std::pair<int, int> &r) {
  return l.second < r.second;
};
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, decltype(cmp)> pq(cmp);

The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type -- called the closure type ...

此外,当重载关系运算成对时,可以把一个运算符的比较工作委托给另外一个已经实现的运算,例如><


访问修饰符

publicprivateprotected。一个类可以有多个标记区域,有效区间为当前位置到下一个访问修饰符。


函数重载与运算符重载

函数的参数个数不同。函数的参数类型不同或者参数类型顺序不同。

  • 运算符重载,算法题常用的是自定义对象(或结构体)的比较,比如用于优先队列的重载有三种实现方式

构造函数相关

  • 拷贝构造函数

    用途:(1) 通过使用另一个同类型的对象来初始化新创建的对象;(2) 复制对象把它作为参数传递给函数;(3) 复制对象,并从函数返回这个对象。

    如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。


友元函数

[ C++之友元机制(友元函数和友元类) ]

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。


匿名函数Lambda

lambda表达式如何捕获成员变量 - 亚九 - 博客园 (cnblogs.com)

[capture list] (params list) mutable exception-> return type { function body }

基本形式如上,capture list:捕获外部变量列表,params list:形参列表,mutable指示符:用来说用是否可以修改捕获的变量,exception:异常设定,return type:返回类型,function body:函数体。


C/C++的可变长参数

[ 可变长参数 ]

  • C

    C语言通过三个宏(va_start、va_end、va_arg)和一个类型(va_list)实现,__VA_ARGS__(C99?)在调用时替换成可变参数。由于无类型检查,存在安全问题,并且不能自动识别可变参数的数量

    #include <stdarg.h>
    #define debug(...) printf(__VA_ARGS__)
    
    int stdarg_counter(int count, ...) {
      int sum = 0;
      //这是一个适用于 va_start()、va_arg() 和 va_end() 这三个宏存储信息的类型
      std::va_list args;
      //这个宏初始化args变量,它与 va_arg 和 va_end 宏是一起使用的。
      //第二个参数count是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
      va_start(args, count);
      for (int i = 0; i < count; ++i) {
        //这个宏检索函数参数列表中类型为 type 的下一个参数
        sum += va_arg(args, int);
      }
      //这个宏允许使用了 va_start 宏的带有可变参数的函数返回。
      //如果在从函数返回之前没有调用 va_end,则结果为未定义。
      va_end(args);
      return sum;
    }
  • C++

    C++可以使用std::initializer_list参数列表模板参数包(变长模板variadic templates)。可变长参数最需要关注的是如何展开可变参数,一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。

    #include <initializer_list>
    #include <iostream>
    
    template <typename T>
    T list_counter(std::initializer_list<T> args) {
      T temp = T();
      for (const T &item : args) {
        temp += item;
      }
      return temp;
    }
    
    int main() {
      std::cout << list_counter<double>({1, 2, 3.0, 4.5}) << std::endl;
      //  1.5
      system("pause");
      return 0;
    }
  • Parameter Pack 让编译器能够轻松地唯一地确定包到底有多大

    [ 特殊情况 ] [ C++17 Deduction Guide ]

    [ 泛化之美--C++11可变模版参数的妙用c++11之函数参数包展开 - 博客园 ] (这两篇总结得非常好)

    // 递归解包
    // 需要考虑递归效率,编译时也需要生成不少版本的_write  (重载)
    template <typename T>
    void _write(const T& t) {
      cout << t << '\n';
    }
    
    template <typename T, typename... Args>
    void _write(const T& t, Args... args) {
      cout << t << ',';
      _write(
          args...);  //递归解决,利用模板推导机制,每次取出第一个,缩短参数包的大小。
    }
    
    template <typename T, typename... Args>
    inline void write_line(const T& t, const Args&... data) {
      _write(t, data...);
    }
    
    // 第二种
    // 参数包在展开的时候,是从右(结束)向左(开始)进行的
    // 所以unpacker(data)...所打印出来的东西可能是反序的
    // 这与编译器的具体实现相关(gcc会,clang不会)
    template <typename... T>
    void DummyWrapper(T... t) {} // 编译器会优化掉
    
    template <class T>
    T unpacker(const T& t) {
      std::cout << ',' << t;
      return t;
    }
    
    template <typename T, typename... Args>
    void write_line1(const T& t, const Args&... data) {
      std::cout << t;
      //直接用unpacker(data)...是非法的,(可以认为直接逗号并列一堆结果没有意义)
      DummyWrapper(unpacker(data)...);
      //也可以使用 int arr[] = {(unpacker(data), 0)...};
      //所以需要用一个函数包裹一下,就好像这些结果后面还有用
      std::cout << '\n';
    }
    
    //C++ 17
    template <typename T, typename... Args>
    void write_line2(const T& t, const Args&... data) {
      std::cout << ',' << t;
      (unpacker(data), ...);  //展开成(((unpacker(data_1), unpacker(data_2)),
                              // unpacker(data_3), ... ),unpacker(data_n)
      std::cout << '\n';
    }

可变模版参数类

[ 泛化之美--C++11可变模版参数的妙用 ]

可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类。


类的静态成员

[ 成员变量的初始化 ]

  • 静态成员为所有类对象所共享,不属于某个具体的实例;

  • 静态成员必须在类外定义,定义的时候不添加static关键字;

  • 类静态成员即可用类名::静态成员或者对象.静态成员来访问;

  • 静态成员函数没有隐藏的this指针,不能访问任何静态成员;

  • 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值、const修饰符等参数。


std::bind与std::function

[ 参考资料 ]

结论:在C和C++中,函数指针非常重要,关注==指向成员函数的指针==。

#include <iostream>
struct Foo {
    int value;
    void f() { std::cout << "f(" << this->value << ")\n"; }
    void g() { std::cout << "g(" << this->value << ")\n"; }
};
void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
    (foo1->*fun)();  // call fun on the object foo1
    (foo2->*fun)();  // call fun on the object foo2
}
int main() {
    Foo foo1{1};
    Foo foo2{2};
    apply(&foo1, &foo2, &Foo::f);
    apply(&foo1, &foo2, &Foo::g);
}

  • 成员函数指针的定义:void (Foo::*fun)(),调用是传递的实参: &Foo::f;注意此时复制不可省略&符号。

  • fun为类成员函数指针,所以调用是要通过解引用的方式获取成员函数*fun,即(foo1->*fun)();


结构体赋值方法

主要包括:定义时赋值、定义后逐个赋值、定义时乱序赋值(C风格)、定义时乱序赋值(C++风格) [ 参考 ] [ 结构体内存对齐 ]

  • C风格

    struct InitMember test = {
        .second = 3.141590,
        .third = "method three",
        .first = -10,
        .four = 0.25
    };
  • C++风格

    struct InitMember test = {
        .second = 3.141590,
        .third = "method three",
        .first = -10,
        .four = 0.25
    };

逻辑&&、||

写代码的时候,递归中返回布尔值的时候,需要注意“逻辑短路”问题。

bool dfs(vector<vector<int>> &grid, int x, int y, const int n, const int m) {
  if (x < 0 || x >= n || y < 0 || y >= m) {
    return false;
  }
  if (grid[x][y] == 1) return true;
  grid[x][y] = 1;
  bool flag = true;
  for (const auto &dir : dirs) {
    // 这行代码被优化了,别用,“或”逻辑前面为1,“与”逻辑前面为0就会发生短路,dfs不会继续进行
    // flag = flag && dfs(grid, x + dir[0], y + dir[1], n, m);
    if (!dfs(grid, x + dir[0], y + dir[1], n, m)) {
      flag = false;
    }
  }
  cout << endl;

  return flag;
}

C++中内存管理(存疑)

结构)在C++中,内存分成4个区,他们分别是堆(自由存储区C++概念)、栈、全局/静态存储区和常量存储区。 (问,堆(malloc/free)和自由存储(new/delete)的区别吗,自由存储区可以借堆实现)(代码区 算运行时的第五个区?)

  • (管理)指针(申请释放过程)、构造函数与析构函数、智能指针.

  • 在C++中有两种创建对象的方式:栈上Object obj或自由存储区(堆)上Object *pt = new Object()

  • 局部静态变量和全局静态变量都是编译时分配空间,区别在于作用域,局部静态变量仅对定义它的作用域(例如,函数)可见。

  • C++中delete this是对象先析构还是delete语句先返回?

    先析构。[ delete this合法,但需要满足一定条件 ]

  • 如何定义一个只能在堆上(栈上)生成对象的类?


智能指针(管理堆内存)

C++11中的智能指针:unique_ptrshared_ptrweak_ptr。方便管理==堆内存==,避免内存泄漏。其原理是,在智能指针对象析构时,会自动释放堆内存,若其指向的堆内存存在特殊的回收机制,可以自定义析构函数。

  • unique_ptr

    独占型指针,不允许拷贝和赋值。禁止拷贝语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr。如果需要转移内存的持有权,可以使用std::move移动构造。

    C++14才支持std::make_unique初始化方式

    std::unique_ptr<int> uptr = std::make_unique<int>(123);
    // C++11标准库中未提供,可采用如下实现
    template<typename T, typename... Ts> // T为对象类型,Ts为对象初始化参数
    std::unique_ptr<T> make_unique(Ts&& ...params) {
        return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
    }
    // 用法
    unique_ptr<Node> up = MakeUnique<Node>(99, 0, 0);
  • shared_ptr

    堆内存资源可以在多个shared_ptr之间共享,该智能指针包含了一个资源引用计数器(多线程安全),并提供use_count()来获取引用计数。

    std::make_shared

  • weak_ptr

    std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助std::shared_ptr工作(例如,可用来解决两个std::shared_ptr相互引用时的死锁问题)。[ 参考博客 ]

    std::weak_ptr不控制资源生命周期,但提供了`expired()方法来检测引用的资源是否存在。也可以通过lock()方法的返回值判断,若对象存在则返回一个shared_ptr,若不存在则返回nullptr。另外,std::weak_ptr并没有重载operator->operator*operator!,因此无法直接通过引用对象来判断是否存在。

    典型例子:订阅者模式或者观察者模式

若类继承自std::enable_shared_from_this,则可以通过return_from_this()返回当前对象指针this(但是不应该共享栈对象的 this 给智能指针对象;应当避免循环引用,即一个资源A的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给整个资源A来管理)。

  • 使用智能指针管理数组

    [ 如何使用智能指针管理数组管理动态数组shared_ptr和动态数组 - apocelipes - 博客园不支持下标运算和指针算数时如何获取数组元素 ]

    shared_ptr和unique_ptr均可以通过自定义deleter(删除器)的方式实现。

    // 方式1
    auto Deleter = [](Connection *connection) { delete[] connection; };
    Connection *c1 = new Connection[2]{string("c1"), string("c2")};
    // 新建管理连接Connection的智能指针
    unique_ptr<Connection, decltype(Deleter)> up(c1, Deleter);
    
    // 方式2
    Connection *c1 = new Connection[2]{string("c1"), string("c2")};
    // 新建管理连接Connection的智能指针
    unique_ptr<Connection[]> up(c1);
    // 方式1的局限在于不支持[]和ptr运算
    shared_ptr<int> sp(new int[10], [](int *p){delet[] p})
    for (size_t i = 0; i != 10; i++) *(sp.get() + i) = i;  //使用get获取动态数组的第一个元素的指针
  • unique_ptr和shared_ptr模板参数上的区别

    [ 为什么unique_ptr的Deleter是模板类型参数,而shared_ptr的Deleter不是?C++ 智能指针自定义删除器 - CSDN博客 ]

    A *pA = new A[3]{1, 2, 3};
    A *pA2 = new A[2]{1, 2};
    
    auto arr_deleter = [](A *arr) { delete[] arr; };
    
    unique_ptr<A, decltype(arr_deleter)> uptr(pA, arr_deleter);
    shared_ptr<A> sptr(pA2, arr_deleter);

    unique_ptr的第二个模板类型参数是Deleter,而shared_ptr的Delete则只是构造函数参数的一部分。出于效率考虑,unique_ptr的设计目标之一是尽可能的高效,如果用户不指定Deleter,就要像原生指针一样高效


::作用域符

  1. global scope(全局作用域符),用法(::name)

    char c = '1';
    
    void demo() {
      char c = '2';
      cout << static_cast<char>(c + 1) << endl;
      cout << static_cast<char>(::c + 1) << endl;
    }
  2. class scope(类作用域符),用法(class::name)

  3. namespace scope(命名空间作用域符),用法(namespace::name)

    PS:{}可以用于在函数中限定局部变量或对象和的作用域,出了作用域则无法访问、执行析构。


using的用法

[ 其它用法 ]

  • 函数别名与模板别名

  • 命名空间与空间成员

  • 派生类中使用基类同名函数

    struct A {
      A(int i) {}
      A(double d,int i){}
      A(float f,int i,const char* c){}
      //...等等系列的构造函数版本号
    };
    struct B:A {
      using A::A;
      //关于基类各构造函数的继承一句话搞定
      //......
    };

namespace命名空间

[ C++命名空间:默认命名空间与匿名命名空间 - CSDN ]


结构体的四种初始化方法

struct InitMember {
    int first;
    double second;
    char* third;
    float four;
};
  • 定义时赋值

    struct InitMember test = {-10,3.141590"method one"0.25};
  • 定义后逐个赋值

    struct InitMember test
    
    test.first = -10;
    test.second = 3.141590;
    test.third = "method two";
    test.four = 0.25;
  • C风格乱序赋值

    struct InitMember test = {
        .second = 3.141590,
        .third = "method three",
        .first = -10,
        .four = 0.25
    };
  • C++风格乱序赋值

    struct InitMember test = {
        second:3.141590,
        third:"method three",
        first:-10,
        four:0.25
    };

非类型类模板参数(Nontype Class Template Parameters)


Pimpl惯用法解析

(pointer to implementation, 指向实现的指针


定义在类内部的类型,需要只用下面的方法获取

[ stackoverflow ]

// ListNode定义在MyLinkedList内部
MyLinkedList::ListNode *MyLinkedList::findNode(int index) {
  // 确保输入的index合法
  int cnt = 0;
  ListNode *curr = hair;
  while (cnt < index) {
    curr = curr->next;
    ++cnt;
  }
  return curr;
}

typename的几种用法


关于拷贝构造函数为什么可以访问另一个对象的私有成员

[ 解释 ]

封装是编译期的概念,是针对类型而非对象的,在类的成员函数中可以访问同类型实例对象的私有成员变量


虚函数

[ C++基类的析构函数为何要声明为虚函数 - 知乎⭐C++中的虚函数表实现机制与内存布局解析 ]

  • 析构函数可定义为虚函数。基类指针可以指向派生类的对象(多态性),如果删除该指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。析构函数不定义为虚函数时,编译器实施静态绑定,在删除基类指针时只会调用基类析构函数,导致析构不完全。
  • 构造函数不能是虚函数。构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。构造函数调用完成后才会形成虚函数表指针。
  • 静态函数、内联函数、非成员函数不可定义为虚函数。

纯虚函数与抽象基类

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。(作用是提供接口)


强制类型转换运算符

  • static_cast

    用于非多态类型的转换,例如数值类型转换,转换时不执行运行时类型检查。父类向派子类转化不安全,子类向父类转化安全。

  • dynamic_cast

    用于多态类型转换,仅用于指针和引用,转换时执行运行时类型检查。可在父类与子类间转换。

  • const_cast

    用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )

  • reinterpret_cast

  • bad_cast

    由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常

  • RTTI查看运行时类型信息

    使用头文件typeinfo。

    typeid 运算符允许在运行时确定对象的实际类型,可返回一个 type_info 对象的引用。


C/C++ 函数调用约定

[ C/C++ 函数调用规范 - 知乎 ]


静态成员不能在类内初始化

[ 为什么类中静态(static)成员不能在类的定义内初始化C++ 类的静态成员static、const 和 static const成员变量声明以及初始化(类内初始化是C++11的新特性)、C++ 类中特殊成员变量(常量、静态、引用)的初始化方法]

  class Test {
 public:
  Test() : a(0) {}
  enum { size1 = 100, size2 = 200 };

 private:
  const int a;        //只能在构造函数初始化列表中初始化
  static int b;       //在类的实现文件中定义并初始化
  const static int c;  //与 static const int c;相同
};

int Test::b=0;  //static成员变量不能在构造函数初始化列表中初始化,因为它不属于某个对象
cosnt intTest::c=0;  //注意:给静态成员常量赋值时,不需要加static修饰符,但要加cosnt

静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。注意 static 关键字只能用于类定义体内部的声明中,定义时不能标示为 static。


C++异常处理

[ C++析构函数默认是noexcept ]


#与##用途

#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。

##连接符号由两个井号组成,其功能是在带参数的宏定义中将两个子串(token)联接起来,从而形成一个新的子串。但它不可以是第一个或者最后一个子串。所谓的子串(token)就是指编译器能够识别的最小语法单元。


vector的内存结构

[ 关于vector变量名的地址和元素地址不相同的问题 ]

在C++中,vector的对象存放在栈区,元素存放在堆区,且变量名地址中存放的是堆区元素的首地址。


右值返回值

[ c++17具名返回值优化NRVO ]


std::bind的返回类型

  • std::bind返回的function类型,一要看被绑定函数的参数列表,二要看bind有没有指定参数。如下所示:

    int func(int a, int b) { return a + b; }
    std::function<int(int)> f1 = std::bind(&func, std::placeholders::_1, 10);
    std::function<int()> f0 = std::bind(&func, 90, 10);
  • std::bind按值拷贝以及std::ref的用处

    int x = 1;
    // 显示利用std::ref来进行引用绑定,可以发现x的值变化了
    auto f = std::bind([](int &y) { y = 99; }, std::ref(x));
    // 此处是按值拷贝,所以并不会修改x的值
    // auto f = std::bind([](int &y) { y = 99; }, x);
    f(); 
    std::cout << x << std::endl;

std::future

[ C++11 并发指南四( 详解三 std::future & std::shared_future) ]

std::future可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。std::future 通常由某个 Provider 创建,此处Provider相当于一个异步任务的提供者。Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。


如何理解面向对象(C++)中的封装、继承、多态?

[Bjarne Stroustrup's FAQ(中文版) ]

[ 这样理解面向对象的封装,继承,多态是否正确? - DarkZero的回答 - 知乎 ]

封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,或者叫接口,同时实现了信息隐藏。

继承:其一、类间继承可以实现代码复用(单继承、多重继承(虚基类));其二、表明兼容性,即派生类与基类的接口完全兼容(抽象基类ABC,规范接口但无需实现)。

多态: 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑表现为多种状态(子类型多态)。

广义上的多态如下:

  1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
  2. 子类型多态(Subtype Polymorphism,运行期):虚函数
  3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
  4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

泛型编程

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。泛型编程的好处是在算法与参数类型无关的情况下,可以避免重复的代码(参数多态)。在C++中的体现时函数模板和类模板,并且C++中提供了更加完备的标准模板库STL。



本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!