C++初级内容理解

发布于 2020-03-06  542 次阅读


第一部分

这部分以类的操作为主。重点在于应付面试中关于C++类的提问。

类封装

和使用函数来操作数据不同,一个对象会有许多的方法,这些方法就相当于代替了操作这堆数据的函数;方法可以重写或保持私有从而能保持安全性。

类继承

一个对象所属的类可以被其派生类继承,继承的类会保持父类的public部分。

注意在继承时,当我们构造子类对象时,会自动调用父类的空构造函数;否则得用子类构造函数加上:来引入父类非空构造函数。

子类不能直接操控父类的private部分,但是可以操控protected部分。

如果我们需要在另一个类使用这个类的private参数,需要在这个类本身添加public的友元函数或友元类。具体实现就是加上一个friend。这样就可以从外部使用private了。可以用友元函数或友元类来修改私有数据。具体在类中,友元类和函数的位置不重要,在哪都行。还有一点就是友元关系不能被继承,也不能传递或反向。

继承时可以选择public(公有继承)或者private(私有继承),私有继承会使得继承该派生类的子类无法访问派生类父类的公有和保护成员;公有则可以访问。

类多态

父类使用虚函数,从而可以使用dynamic_cast<子类*>来实现动态类型转换。这两个我认为一般可以配合起来用。

虚函数指的是父类弄一个子类一定都得有的函数,但是父类本身因为抽象程度高所以不提供实现。纯虚函数的具体操作就是加个virtual XXX() = 0;,这样就不用写父类的虚函数实现;如果是普通的虚函数就在父类写个声明和实现而不是直接赋值成0。

子类需要实现父类的虚函数,给一个具体的定义。当然也可以不实现,不过在调用这个虚函数对应方法的时候就会出错,如果不调用就不会出错。

这样的好处是配合动态类型转换来实现一个通用函数可以操作多种派生类的对象。

注意纯虚函数抽象类无法直接产生一个新的对象,必须是这个类的一个非纯虚函数的派生类才可以。

细节

构造函数函数名是类名,析构函数要前面加上一个~

static变量的初始化在类外进行;私有类static变量初始化类似全局变量初始化。

类成员变量利用列表初始化时顺序和其在内存中顺序有关,并非和写起来的顺序有关。

构造函数类似俄罗斯套娃, 由父类一层层套到子类;而析构函数则从子类一层层析构回父类。

拷贝构造函数要写成XXX::XXX(XXX &)的样子。这个是浅拷贝,深拷贝就需要重写方法。

基类的virtual函数如果不是纯虚函数,那么就必须要有一个实现(哪怕实现是空的),不然ld连接会报错。

注意保持声明和实现分离,实现在cpp中,声明在h文件中,利用ifndef来防止重复引用。编译时注意要加入多个cpp文件组合编译。

内联函数本质是把函数体压入系统栈从而提高效率,所以比较适合短小的函数使用内联,长函数不适合强行内联。

还有一个细节:尽量不要在构造函数中调用虚函数或在无法报错的情况下使用可能操作失败的函数,否则可能产生不可用的错误。建议在工程实践中使用工厂模式来解决这个问题。

其他细节待补充(C++智能指针实现和向量类之类的)

示例

写了一个小示例,应该包含了上面说的比较多的情况。

account.h

#ifndef ACCOUNT_H
#define ACCOUNT_H
class Account{
    private:
        static int num;
        int id;
        double balance,annualInterestRate;

    public:
        friend class CheckingAccount;
        Account();
        Account(int id,double balance,double annualInterestRate);
        ~Account();
        int getId() const;
        void setId(int id);
        double getBalance() const;
        void setBalance(double balance);
        double getAnnualInterestRate() const;
        void setAnnualInterestRate(double);
        double getMonthlyInterestRate() const;
        virtual bool withDraw(double money){};
        virtual void deposit(double money){};
        virtual inline void toString() const;
};

class CheckingAccount:public Account{
    private:
        double access;

    public:
        CheckingAccount();
        CheckingAccount(int id,double balance,double annualInterestRate);
        ~CheckingAccount();
        bool withDraw(double money);
        void deposit(double money);
        void toString() const;
};

class SavingAccount:public Account{
    private:
        double access;

    public:
        SavingAccount();
        SavingAccount(int id,double balance,double annualInterestRate);
        ~SavingAccount();
        bool withDraw(double money);
        void deposit(double money);
        //inline void toString() const; /* test child class not rewrite the father class virtual toString() */
};

#endif // By Init_new_world

account.cpp

#include"account.h"
#include<iostream>
using namespace std;

int Account::num = 0;

Account::Account(){
    cout << "Account construction 1" << endl;
    num ++;
    id = 0;
    balance = annualInterestRate = 0.0;
}

Account::Account(int id,double balance,double annualInterestRate){
    cout << "Account construction 2" << endl;
    num ++;
    this -> id = id;
    this -> balance = balance;
    this -> annualInterestRate = annualInterestRate;
}

Account::~Account(){
    cout << "Account destruction" << endl;
    num --;
}

int Account::getId() const{
    return this -> id;
}

void Account::setId(int id){
    this -> id = id;
}

double Account::getBalance() const{
    return this -> balance;
}

void Account::setBalance(double balance){
    this -> balance = balance;
}

double Account::getAnnualInterestRate() const{
    return this -> annualInterestRate;
}

void Account::setAnnualInterestRate(double annualInterestRate){
    this -> annualInterestRate = annualInterestRate;
}

double Account::getMonthlyInterestRate() const{
    return this -> annualInterestRate / 12.0;
}

inline void Account::toString() const{
    cout << "Account Class toString!" << " " << "Number of all accounts:" << this -> num << endl;
}

CheckingAccount::CheckingAccount(){
    cout << "CheckingAccount construction 1" << endl;
    access = 1000.0;
}

CheckingAccount::CheckingAccount(int id,double balance,double annualInterestRate):Account(id,balance,annualInterestRate){
    cout << "CheckingAccount construction 2" << endl;
}

CheckingAccount::~CheckingAccount(){
    cout << "CheckingAccount destruction" << endl;
}

bool CheckingAccount::withDraw(double money){
    if(this -> balance + this -> access < money){
        return false;
    }
    this -> access -= money;
    if(this -> access < 0){
        this -> balance += this -> access;
        this -> access = 0.0;
    }
    return true;
}

void CheckingAccount::deposit(double money){
    this -> access += money;
    if(this -> access > 1000.0){
        this -> balance += this -> access - 1000.0;
        this -> access = 1000.0;
    }
}

inline void CheckingAccount::toString() const{
    cout << "CheckingAccount toString!" << " " << this -> balance << endl;
}

SavingAccount::SavingAccount(){
    cout << "SavingAccount construction 1" << endl;
    access = 0.0;
}

SavingAccount::SavingAccount(int id,double balance,double annualInterestRate):Account(id,balance,annualInterestRate){
    cout << "SavingAccount construction 2" << endl;
}

SavingAccount::~SavingAccount(){
    cout << "SavingAccount destruction" << endl;
}

bool SavingAccount::withDraw(double money){
    if(this -> getBalance() + this -> access < money){
        return false;
    }
    this -> access -= money;
    if(this -> access < 0){
        this -> setBalance(this -> getBalance() + this -> access);
        this -> access = 0.0;
    }
    return true;
}

void SavingAccount::deposit(double money){
    this -> access += money;
    if(this -> access > 0.0){
        this -> setBalance(this -> getBalance() + this -> access);
        this -> access = 0.0;
    }
}
/**
test child class not rewrite the father class virtual toString()
*/
/*
inline void SavingAccount::toString() const{
    cout << "SavingAccount toString!" << " " << this -> getBalance() << endl;
}
*/

object_test.cpp

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include"account.h"
using namespace std;

void WhichAccount(Account *z){
CheckingAccount *p = dynamic_cast<CheckingAccount *> (z);
SavingAccount *q = dynamic_cast<SavingAccount *> (z);
if(p != NULL){
    cout << "CheckingAccount!" << endl;
}
if(q != NULL){
    cout << "SavingAccount!" << endl;
}
}

int main(){
CheckingAccount *ik = new CheckingAccount(1122,2000.0,4.5);
cout << ik -> withDraw(4500) << endl;
cout << ik -> withDraw(2500) << endl;
cout << ik -> getId() << " " << ik -> getBalance() << " " << ik -> getMonthlyInterestRate() << endl;
ik -> toString();
SavingAccount *pk = new SavingAccount();
WhichAccount(pk);
WhichAccount(ik);
delete pk;
delete ik;
return 0;
}

测试效果:

obj_test.png


第二部分

这部分关于C++的基础知识,以及一些小的细节、奇妙的操作。

C文件IO/读写/编码方面

主要使用的函数可以有fscanfsscanfseek_tscanf

wchar_t类型宽字符类,可以有效的处理中文等UTF-8宽字符编码。注意一个汉字在UTF-8中占3个字节。

fopenC文件打开函数,可以创建一个FILE *指针

seek移动文件指针,三个参数:一个FILE *指针,第二个数字是与对比基准的偏移量,第三个是对比基准,有三个(SEEK_CUR当前位置,SEEK_END文件结尾,SEEK_SET文件开头)

_tscanf宽字符读入方式,当然scanf也可以,读入宽字符串可以用%ls

sscanf对于一个字符串进行格式化再读入,理解成用一个字符串来当作scanf的输入;

cast类型转换

主要分为四种cast转换:const_caststatic_castdynamic_castreinterpret_cast

const_cast:常量转非常量

static_castconst_cast加上隐式转换(比如void指针),也可以多态向上转换,可以向下转换但是有风险。

dynamic_cast:虚函数转换,加上多态向上/下转换,错误会出NULL

reinterpret_cast:啥都能转,不过可能有风险。

类的静态成员函数

静态成员函数类似类变量,可以直接被类名::静态成员函数()直接引用,不需要额外新建对象。

C++智能指针

C++中有四种智能指针:auto_ptr(在C++11中已弃用)shared_ptrunique_ptrweak_ptr

auto_ptr:独占式拥有(指向某一个对象后可以被替换掉,替换后原指针不再指向那个对象,存在潜在的数据非法访问问题)

unique_ptr:独占式拥有(指向某一个对象后,不能被赋值给其他的unique_ptr(除了使用std::move函数来移动),但是作为等式右边的值则可以)

shared_ptr:共享式拥有,多个智能指针可以指向同一个对象,只有最后一个指针被销毁后对象才完全被销毁。可以通过成员函数use_count()来查询当前对象被多少个智能指针指向。用release函数(注意这个不是成员函数,因为操作的是同一块内存空间)释放当前指针占用(计数器减1),unique成员函数查询是否独占所有权。

一些细节:链接

weak_ptr:解决shared_ptr引用中出现的交叉引用(死锁)问题。相当于引用不增加版的shared_ptr;注意我们不能通过weak_ptr直接访问对象的方法,需要转换成shared_ptr才行。

C++函数指针

我们可以通过函数指针来实现函数作为一个参数被传入另一个函数。

比如类型(*函数指针名)(函数参数1,函数参数2,...)

这个指针就可以被作为参数传入。

类mutable关键字

使用mutable,可以在类的const方法中修改值。这个参数是用来修饰那个被修改的值的。

在main函数运行之前执行一些函数

方法1

__attribute() void before_main(){}

方法2

全局变量初始化的时候,用一个函数初始化,这个函数便会在main函数之前执行

extern "C"

临时调用C语言来编译C++程序,仅限这句话在的那一部分。

RTTI(运行时内存检查)

只有在cast的时候会用到,暂时不管

C++ struct

C++的struct和C的struct不一样,C++中struct可以部分代替class,不过struct默认均为public,class默认均为private;还有一点就是模板类形参必须使用class。

C++11右值引用

链接

brk内存分配和mmap内存分配

brk内存分配直接指向堆顶,一般是一个比较小(小于128K)的空间。

mmap内存分配是在堆和栈中间找一块内存进行分配,一般是动态库或者大于128K的空间分配。

第三部分

这部分是C++的主要部分,虽然只是很少的一部分,代表了我应付面试时的准备。

C++异常处理

使用try...catch来操作异常。

例如

#include <iostream>
using namespace std;

double division(int a, int b)
{
   if( b == 0 )
   {
      throw "Division by zero condition!";
   }
   return (a/b);
}

int main ()
{
   int x = 50;
   int y = 0;
   double z = 0;

   try {
     z = division(x, y);
     cout << z << endl;
   }catch (const char* msg) {
     cerr << msg << endl;
   }

   return 0;
}

定义新的异常

#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};

int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

如果仅仅是处理所有异常,在catch中使用省略号...即可。这样会捕获所有上面catch(如果有的话)没有接收的异常。

例如

#include <iostream>
using namespace std;

double division(int a, int b)
{
   if( b == 0 )
   {
      throw "Division by zero condition!";
   }
   return (a/b);
}

int main ()
{
   int x = 50;
   int y = 0;
   double z = 0;

   try {
     z = division(x, y);
     cout << z << endl;
   }catch (...) {
     cerr << "Error" << endl;
   }

   return 0;
}

cout/cerr/clog区别

cout:标准输出流,输出到std指定的流。可以使用ofstream重定向。

cerr:非缓冲错误流。这个流可以将错误立刻输出到屏幕上,是实时的、非缓冲的。

clog:缓冲错误流。这个流相当于具有缓冲区的cerr。

重定向使用对应流的rdbuf函数。

fork/vfork写时复用

fork:创建一个新的子进程,复制父进程的所有数据。

在现代Linux系统中,采用了“写时复用”,所以fork的性能很大的提升了。(平常复用,产生改变时才复制)

vfork:创建一个新的子进程,复用父进程的所有数据。

从性能上说,vfork更优一些。但是,vfork主要的作用是用于立刻执行execve来实现类似shell的功能,如果在vfork中return 0,程序就会崩溃,而且现在已经对fork的机制进行了改善。所以平时使用还是尽量使用fork。

参考文章

内存泄漏定位

在Linux下使用valgrind工具,用法

valgrind --leak-check=full ./your_program

根据输出信息可以直接定位泄露地点。

C++重载和重写

重载是函数或方法和原来的函数或方法输入参数不一致;

重写就是一致的。

C++ constexpr

常量表达式,在编译阶段进行操作;

例如

int Hello(){return 10;}
constexpr int a=2*Hello()+1;

在运算a的时候,会在编译阶段运算而非运行阶段,从而提高效率。

相对于宏命令,更加安全、可靠。同时是一种强约束,可以避免语义被破坏。

C++ cout执行顺序

对于cout中的参数,计算顺序是从右往左,而输出顺序是从左往右;

例如

int fun1(){
 cout<<"num1"<<endl;
 return 1;
}

int fun2(){
 cout<<"num2"<<endl;
 return 2;
}

int main(){
 cout<<fun1()<<"  "<<fun2()<<endl;
 return 0;
}

输出结果是

num2
num1
1  2

C++ fork复制细节

fork子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段。

就是数据都会复制,但是本身执行代码是共享的。

C++ vector reserve/resize

使用reserve来提前分配一定大小的空间;

resize则是提前分配一定大小的空间并把这些空间填充上元素(不给则使用默认构造函数)。

C++ 函数签名

函数签名用于唯一标识每一个函数。函数签名包含了一个函数的信息,包括函数名、参数类型、参数个数、顺序以及它所在的类和命名空间,但是不包含函数的返回值。如果两个函数只有返回值不一样,编译器会报错。

C++ new[]/delete[]原理

本质上,newdelete是两个C++中的函数,我们使用delete[]的时候,调用析构函数的次数取决于这个指针的前面的4个字节,这4个字节里面保存着数组的长度,也就是调用析构函数的次数。

C++ 析构函数虚函数

构造函数不可以是虚函数,基类析构函数几乎一定要是虚函数。这样可以避免在使用多态的特定时候只析构基类本身而不析构派生类。

这种情况具体出现在使用基类指针来实例化一个派生类对象的时候,由于是基类指针导致只会析构基类。如果是虚函数则会通过虚函数表去找派生类析构。

顶层const和底层const

顶层const指指针本身是一个常量,指针内容不能修改;

int val = 10;
int *const p = &val;

底层const指指针指向一个常量,指针内容可以修改。

const int *p = &val;

迭代器erase细节

在使用迭代器的时候,如果erase的话要把当前迭代器更新为erase的返回值,因为迭代器本身被删除了,erase返回这个迭代器的下一个迭代器,所以需要修改。

using类型别名

可以使用using指向一个类型别名,类似typedef。

using int = IT;
IT x = 9;

long long

一个奇妙的小知识:long long在C++11中才被写入C++标准。

第四部分

这部分是我在入职后,对于公司文档和编码规范的部分理解,以及一些工程实践的细节。

C++ tuple

C++元组是一个泛化后的std::pair。这个操作的方式一般为引用,可以当成一个简化的结构体或增强版的pair。

由于相当于元素个数更多的pair,在使用过程中,我们可以利用decltype来自动推断元组类型来实现一种元素个数统计。

std::tuple<int, char, double> mytuple (10, 'a', 3.14);
std::tuple_size<decltype(mytuple)>::value;

得到元素的值的方法我们可以使用std::get<0>(mytuple)的方法,这是得到第一个值。在C++14中我们引入了std::get来在类型唯一时直接取类型,但是在工程实践中我们一般更多的使用自定义结构体来避免访问元素不清晰的结果。

C++ 对象切割

文章

注意对象切割会造成额外的性能损耗,而且并不能实现多态。所以我们在工程实践中经常会禁用基类的拷贝、赋值构造函数,并在派生类中提供自定义的拷贝/赋值构造函数。

C++ 继承-工程实践

尽量少的使用继承。所有的继承最好均为public

对于可能被子类访问的成员函数,不要过度使用protected关键字。注意,数据成员都必须是私有的。

虚函数重载和虚析构函数在使用中需要尽量使用override或者final关键字进行标记,早于C++11的代码则尽量使用virtual进行标记。

C++ 引用参数

注意:所有左值引用的参数必须标记为const

这样在工程实践中有助于在不知情的情况下减少bug。

C++ 返回类型后置语法

在C++11中,出现了这样的一种新的语法:返回类型后置。

普通写法:

int foo(int x);

后置写法:

auto foo(int x) -> int;

后置写法是是显式指定lambda表达式返回值的唯一方法。虽然编译器往往可以自动推断lambda表达式的类型,但是在一些特殊情况下还是需要这么写。

在某些情况下,这么写会更加一目了然。

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

对比前置:

template <typename T, typename U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

不过在工程实践中,一般也不需要这么写。

C++ 完美转发

在某些情况下,使用C++编程会遇到一些问题。比如,我在某个函数内使用了自定义类型进行函数传参,这个自定义类型可能在使用时传参开销会上升。所以我们如果使用了一些局部变量来保存参数,最后返回这个参数,那么可以利用完美转发直接把指针转发过去,从而实现减小传参开销的作用。

文章

引用的一些细节

C++ 自增自减

注意:在不考虑返回值的情况下,使用前置自增/自减比后置自增/自减效率更高。后置自增/自减需要对表达式的值进行一次拷贝。如果对象是迭代器或其他非数值类型,拷贝的代价是比较大的。

C++ 类型转换-工程实践

尽量少的使用dynamic_cast。对于一些转换的流程,我们可以用static_cast,也可以在数值转换上用大括号的方式来转换。

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
    int16_t p=3;
    int64_t a{p};
    printf("%I64d\n",a);
    return 0;
}

第五部分

这部分是在第二年(也就是大三),我在进行面试准备时写作的。

C++ decltype

这个相当于一个宏,可以返回内部经过运算的最终数据类型或函数的返回值类型。这样写在一部分情况下会使得整个程序变得更加清晰。

C++ 内联函数

使用inline来标记一个函数作为内联函数;在编译器不忽略的情况下,这个函数将会在使用的时候被在使用的地方进行展开,并进行运算。(大致类似宏的操作)

C++ 常量表达式

constexpr用于标记常量表达式函数。这些函数的运算结果将在编译时进行运算,从而提高程序运行效率。注意经过该关键字表达的函数返回值不一定是常量,但是这个值一定可以在编译时得到,所以是常量表达式。(也就是说,可以理解成类似没有const标记的变量,即使这个变量的结果不变)

C++ assert

这个语句用于调试中检测不能达到的条件。表达式为假的时候,程序停止运行。

C++ 函数匹配

在具有多种重载的函数进行调用时,我们会选择一种相对最好的调用方法;存在多种情况一样好的话,那么编译器会提示二义性错误从而无法编译。注意精确匹配好于需要转换的匹配。

举个例子

void f(int x,int y);//情况1
void f(double x,double y);//情况2

f(2,3.3);//这里的调用是会被报错的,因为它符合情况1的时候需要经过一次类型转换,符合情况2的时候也需要进行一次类型转换,所以一样的好,从而会报二义性错误

C++ this指针

这个指针在C++类中常用来表示自己本身。其实现方式如下:

一种情况是返回常量;

std::string S::p(const S *const this){
return this->p;
}

/*
std::string p() const {
return p;
}
*/

本身传入的逻辑是一种隐式参数,相当于调用成员函数时传入了自己的指针引用;

//S tot.p()
//=>
//S::p(&tot)

C++ 拷贝细节

C++标准库中,vectorstring在进行拷贝、赋值或销毁操作的时候,会设法赋值内部元素的值;(出现指针则可能无法正常工作)

C++ 友元

使用友元,可以让其他类或者函数访问当前类的非公有成员。一般友元函数之类的声明会放在类的开头。利用友元函数,我们可以方便的实现类似读取和输出类成员的操作(隶属于istreamostream

C++ Lambda

C++ Lambda表达式是在C++ 11中新添加的特性

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

如果需要修改捕获变量,那么就得输入mutable;这个可以构造匿名函数。

注意Lambda表达式不能有可变参数。

It is my final heart.
最后更新于 2020-03-06