关于c++:C基础知识

4次阅读

共计 14886 个字符,预计需要花费 38 分钟才能阅读完成。

原文链接

1 static 关键字

加了 static 关键字的全局变量只能在 本文件 中应用。

static 定义的动态局部变量调配在 数据段 上,一般的局部变量调配在栈上,会因为函数栈帧的开释而被开释掉。

1.1 全局动态变量

在全局变量前加上关键字 static,全局变量就定义成一个 全局动态变量
内存中的地位:动态存储区 ,在整个程序运行期间始终存在。
初始化:未经初始化的全局动态变量会被 主动初始化为 0(主动对象的值是任意的,除非他被显式初始化);
作用域:全局动态变量在申明 仅在本文件可见,他的文件之外是不可见的,精确地说是从定义之处开始,到文件结尾。

1.2 部分动态变量

在局部变量之前加上关键字 static,局部变量就成为一个 部分动态变量
内存中的地位:动态存储区 ,在整个程序运行期间始终存在。
初始化:未经初始化的全局动态变量会被主动初始化为 0(主动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为部分作用域,当定义它的函数或者语句块完结的时候,作用域完结。然而当部分动态变量来到作用域后,并没有销毁,而是 依然驻留在内存当中,只不过咱们不能再对它进行拜访,直到该函数再次被调用,并且值不变;

1.3 动态函数

在函数返回类型前加 static,函数就定义为 动态函数 。函数的定义和申明在默认状况下都是 extern 的,但动态函数 仅在本文件可见 ,不能被其余文件所用。
函数的实现应用 static 润饰,那么这个函数只可在本 cpp 内应用,不会同其余 cpp 中的同名函数引起抵触;
warning:在 头文件中申明非 static 的全局函数,在 cpp 内申明 static 的全局函数,如果你要在多个 cpp 中复用该函数,就把它的申明提到头文件里去,否则 cpp 外部申明需加上 static 润饰;

1.4 类的动态成员

对一个类中成员变量和成员函数来说,加了 static 关键字,则此变量 / 函数就 没有 this 指针 了,必须通过 类名拜访

在类中,动态成员能够实现多个对象之间的数据共享,并且应用静态数据成员还不会毁坏暗藏的准则,即保障了安全性。因而,动态成员是类的所有对象中共享的成员 ,而不是某个对象的成员。对多个对象来说,静态数据成员 只存储一处,供所有对象共用。

1.5 类的动态函数

动态成员函数和静态数据成员一样,它们都属于 类的动态成员 ,它们都不是对象成员。因而,对动态成员的援用不须要用对象名。在 动态成员函数的实现中不能间接援用类中阐明的非动态成员,能够援用类中阐明的动态成员(这点十分重要)。如果动态成员函数中要援用非动态成员时,可通过对象来援用。从中可看出,调用动态成员函数应用如下格局:< 类名 >::< 动态成员函数名 >(< 参数表 >);

2 C++ 和 C 的区别

2.1 设计思维上

C++ 是 面向对象 的语言,而 C 是面向过程的结构化编程语言

2.2 语法上

C++ 具备 重载 继承 多态 三种个性;
C++ 相比 C,减少多许多类型平安的性能,比方强制类型转换;
C++ 反对 范式编程,比方模板类、函数模板等。

3 C++ 中四种 cast 转换

C++ 中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

3.1 const_cast

用于将 const 变量转为 非 const。它也是四个强制类型转换运算符中 惟一 可能去除 const 属性的运算符。对于未定义 const 版本的成员函数,咱们通常须要应用 const_cast 来去除 const 援用对象的 const,实现函数调用。另外一种应用形式,联合 static_cast,能够在非 const 版本的成员函数内增加 const,调用完 const 版本的成员函数后,再应用 const_cast 去除 const 限定。

3.2 static_cast

static_cast< new_type >(expression)
// new_type 为指标数据类型,expression 为原始数据类型变量或者表达式。

根本数据类型之间的转换,如 int、float、char 之间的相互转换;用于各种 隐式转换 ,比方非 const 转 const,void* 转指针等,但 没有运行时类型查看来保障转换的安全性

隐式类型转换 :首先,对于内置类型,低精度的变量给高精度变量赋值会产生隐式类型转换,其次,对于只存在单个参数的构造函数的对象结构来说,函数调用能够间接应用该参数传入,编译器会主动调用其构造函数生成 长期对象

static_cast 次要有如下几种用法:

  1. 用于类层次结构中 基类 派生类 之间指针或援用的转换。
    进行 向上 转换是 平安 的;

    进行 向下 转换时,因为没有动静类型查看,所以是 不平安 的。因为 基类不蕴含派生类的成员变量,无奈对派生类的成员变量赋值。

  2. 用于根本数据类型之间的转换,如 int、float、char 之间的相互转换
  3. 把空指针转换成 指标类型的空指针
  4. 把任何类型的表达式转 换成 void 类型

留神:static_cast 不能 去掉expression 的 const、volatile、或者__unaligned 属性。

char a = 'a'; int b = static_cast<char>(a);  // 将 char 型数据转换成 int 型数据

const int g = 20;
int *h = static_cast<int*>(&g);   // 编译谬误,static_cast 不能去掉 g 的 const 属性
class Base
{};
class Derived : public Base
{}

Base* pB = new Base();
if(Derived* pD = static_cast<Derived*>(pB))
{}  // 上行转换是不平安的(坚定抵制这种办法)

Derived* pD = new Derived();
if(Base* pB = static_cast<Base*>(pD))
{}   // 上行转换是平安的

3.3 dynamic_cast

dynamic_cast< new_type >(expression)
// new_type 为指标数据类型,expression 为原始数据类型变量或者表达式。dynamic_cast< type* >(e)   //type 必须是一个类类型且必须是一个无效的指针 
dynamic_cast< type& >(e)   //type 必须是一个类类型且必须是一个左值 
dynamic_cast< type&& >(e)  //type 必须是一个类类型且必须是一个右值

用于 动静类型转换 。只能用于 含有虚函数的类 ,用于类档次间的 向上 向下 转化、类之间的 穿插转换 (cross cast)。只能转 指针 援用

在类档次间 向上 转换时,dynamic_cast 和 static_cast 的成果是一样的;在进行 向下 转换时,dynamic_cast 具备 类型查看 的性能,它通过判断在执行到该语句的时候,变量类型和要转换的 类型是否雷同 来判断是否可能进行向下转换,如果是非法的对于转换指标是 指针 类型返回 NULL,对于援用抛 std::bad_cast 异样比 static_cast 更平安。

3.4 reinterpret_cast

简直什么都能够转,比方将 int 转指针,执行的是 一一比特复制 的操作。容易出问题,尽量少用。

3.5 为何不必 C 的强制转换

C 的强制转换外表上看起来功能强大什么都能转,然而转化不够明确,不能进行谬误查看,容易出错。

4 C/C++ 中指针和援用的区别

4.1 指针

指针利用地址,它的值间接指向存在电脑存储器中另一个中央的值。因为通过地址能找到所需的变量单元,能够说,地址指向该变量单元。因而,将地址形象化的称为“指针”。意思是通过它能找到 以它为地址 的内存单元。

4.2 援用

援用就是某一变量的一个 别名,对援用的操作与对变量间接操作齐全一样。援用的申明办法:类型标识符 & 援用名 = 指标变量名;援用引入了对象的一个同义词。定义援用的示意办法与定义指针类似,只是用 & 代替了 *

4.3 区别

  1. 指针有本人的一块空间,而援用只是一个别名;
  2. 应用 sizeof 看一个指针的大小是 4,而援用则是被援用对象的大小;
  3. 指针能够被初始化为 NULL,而援用必须被初始化且必须是一个已有对象的援用;
  4. 作为参数传递时,指针须要被解援用才能够对对象进行操作,而对援用的批改都会扭转援用所指向的对象;
  5. 能够有 const 指针,然而没有 const 援用;
  6. 指针在应用中能够指向其它对象,然而援用只能是一个对象的援用,不能 被扭转;
  7. 指针能够有多级指针(**p),而援用至于一级;
  8. 指针和援用应用 ++ 运算符的意义不一样;
  9. 如果返回动态内存调配的对象或者内存,必须应用指针,援用可能引起内存泄露。

5 C++ 智能指针

C++ 外面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是 c ++11 反对,并且第一个曾经被 11 弃用。

为什么要应用智能指针:
智能指针的作用是 治理一个指针 ,因为存在以下这种状况:申请的空间在函数完结时 遗记开释 ,造成 内存透露 。应用智能指针能够很大水平上的防止这个问题,因为智能指针就是一个,当超出了类的作用域是,类会 主动调用析构函数,析构函数会主动开释资源。所以智能指针的作用原理就是在函数完结时主动开释内存空间,不须要手动开释内存空间。

对 shared_ptr 进行初始化时不能将一个一般指针间接赋值给智能指针,因为一个是指针,一个是类。能够通过 make_shared 函数或者通过构造函数传入一般指针。并能够通过 get 函数取得一般指针。

5.1 auto_ptr

c++98 的计划,cpp11 曾经摈弃。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr 不会报错.

此时不会报错,p2 剥夺了 p1 的所有权,然而当程序运行时拜访 p1 将会报错。所以 auto_ptr 存在潜在的内存解体问题。

5.2 unique_ptr

替换 auto_ptr。unique_ptr 实现 独占式领有 或严格领有概念,保障同一时间内只有一个智能指针能够指向该对象。它对于防止资源泄露 (例如:以 new 创建对象后因为产生异样而遗记调用 delete) 特地有用。还是下面那个例子:

unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4;//#5
p4 = p3;  // 此时会报错!!

编译器认为 p4=p3 非法,防止了 p3 不再指向无效数据的问题。因而,unique_ptr 比 auto_ptr 更平安。另外 unique_ptr 还有更聪慧的中央:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个长期右值,编译器容许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比方:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed

其中 #1 留下悬挂的 unique_ptr(pu1),这可能导致危害。而 #2 不会留下悬挂的 unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创立的长期对象在其所有权让给 pu3 后就会被销毁。这种随状况而已的行为表明,unique_ptr 优于容许两种赋值的 auto_ptr。
注:如果的确想执行相似与 #1 的操作,要平安的重用这种指针,可给它赋新值。C++ 有一个规范库函数 std::move(),让你可能将一个 unique_ptr 赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

5.3 shared_ptr

shared_ptr 实现 共享式领有 概念。多个智能指针能够指向雷同对象,该对象和其相干资源会在“最初一个援用被销毁”时候开释。从名字 share 就能够看出了资源能够被多个指针共享,它应用 计数机制 来表明资源被几个指针共享。能够通过成员函数 use_count()来查看资源的所有者个数。除了能够通过 new 来结构,还能够通过传入 auto_ptr, unique_ptr,weak_ptr 来结构。当咱们调用 release()时,以后指针会开释资源所有权,计数减一。当计数等于 0 时,资源会被开释。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的), 在应用援用计数的机制上提供了能够共享所有权的智能指针。
成员函数:

  • use_count 返回援用计数的个数
  • unique 返回是否是独占所有权(use_count 为 1)
  • swap 替换两个 shared_ptr 对象(即交换所领有的对象)
  • reset 放弃外部对象的所有权或领有对象的变更, 会引起原有对象的援用计数的缩小
  • get 返回外部对象 (指针), 因为曾经重载了() 办法, 因而和间接应用对象是一样的。如:

    shared_ptr<int> sp(new int(1)); // sp 与 sp.get()是等价的

5.4 weak_ptr

weak_ptr 是一种不管制对象生命周期的智能指针, 它指向一个 shared_ptr 治理的对象。weak_ptr 设计的目标是为 帮助 shared_ptr 而引入的一种智能指针,它只能够从一个 shared_ptr 或另一个 weak_ptr 对象结构,它的结构和析构不会引起援用记数的减少或缩小。

weak_ptr 是用来 解决 shared_ptr 互相援用时的死锁问题,如果说两个 shared_ptr 互相援用,那么这两个指针的援用计数永远不可能降落为 0,资源永远不会开释。它是对对象的一种弱援用,不会减少对象的援用计数,和 shared_ptr 之间能够互相转化,shared_ptr 能够间接赋值给它,它能够通过调用 lock 函数来取得 shared_ptr。

class B;
class A
{
    public:
    shared_ptr<B> pb_;
    ~A()
    {cout<<"A delete\n";}
};
class B
{
    public:
    shared_ptr<A> pa_;
    ~B()
    {cout<<"B delete\n";}
};
void fun()
{shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;}
int main()
{fun();
    return 0;
}  

能够看到 fun 函数中 pa,pb 之间相互援用,两个资源的援用计数为 2,当要跳出函数时,智能指针 pa,pb 析构时两个资源援用计数会减一,然而两者援用计数还是为 1,导致跳出函数时资源没有被开释(A B 的析构函数没有被调用),如果把其中一个改为 weak_ptr 就能够了,咱们把类 A 外面的 shared_ptr pb 改为 weak_ptr pb 运行后果如下,这样的话,资源 B 的援用开始就只有 1,当 pb 析构时,B 的计数变为 0,B 失去开释,B 开释的同时也会使 A 的计数减一,同时 pa 析构时使 A 的计数减一,那么 A 的计数为 0,A 失去开释。

留神的是咱们不能通过 weak_ptr 间接拜访对象的办法,比方 B 对象中有一个办法 print(),咱们不能这样拜访,pa->pb->print(); 英文 pb_是一个 weak_ptr,应该先把它转化为 shared_ptr,如:

shared_ptr p = pa->pb_.lock(); 
p->print();

5.5 内存泄露

当两个对象互相应用一个 shared_ptr 成员变量指向对方,会造成 循环援用,使援用计数生效,从而导致内存透露。

#include <iostream>  
#include <memory>  
using namespace std;  
  
class B;  
class A  
{  
public:  // 为了省去一些步骤这里 数据成员也申明为 public  
    shared_ptr<B> pb;  
    ~A()  
    {cout << "kill A\n";}  
};  
class B  
{  
public:  
    shared_ptr<A> pa;  
    ~B()  
    {cout <<"kill B\n";}  
};  
int main(int argc, char** argv)  
{shared_ptr<A> sa(new A());  
    shared_ptr<B> sb(new B());  
    if(sa && sb)  
    {  
        sa->pb=sb;  
        sb->pa=sa;  
    }  
    cout<<"sa use count:"<<sa.use_count()<<endl;  
    return 0;  
}

留神此时 sa,sb 都没有开释,产生了内存泄露问题。即 A 外部有指向 B,B 外部有指向 A,这样对于 A,B 必然是在 A 析构后 B 才析构,对于 B,A 必然是在 B 析构后才析构 A,这就是循环援用问题,违反常规,导致内存泄露。

解决办法

应用弱援用的智能指针 weak_ptr 突破这种循环援用。为了解决循环援用导致的内存透露,引入了 weak_ptr 弱指针,weak_ptr 的构造函数不会批改援用计数的值,从而不会对对象的内存进行治理,其相似一个一般指针,但不指向援用计数的共享内存,然而其能够检测到所治理的对象是否曾经被开释,从而防止非法拜访。

5.6 shared_ptr 的实现

template <typename T>
class SmartPtr
{
private:
    T *ptr; // 底层实在的指针
    int *use_count;// 保留以后对象被多少指针援用计数
public:
    SmartPtr(T *p); //SmartPtr<int>p(new int(2));
    SmartPtr(const SmartPtr<T>&orig);//SmartPtr<int>q(p);
    SmartPtr<T>&operator=(const SmartPtr<T> &rhs);//q=p
    ~SmartPtr();
    T operator*(); // 为了能把智能指针当成一般指针操作定义解援用操作
    T*operator->(); // 定义取成员操作
    T* operator+(int i);// 定义指针加一个常数
    int operator-(SmartPtr<T>&t1, SmartPtr<T>&t2);// 定义两个指针相减
    void getcount()
    {return *use_count}
};

template <typename T>
int SmartPtr<T>::operator-(SmartPtr<T> &t1, SmartPtr<T> &t2)
{return t1.ptr - t2.ptr;}
template <typename T>
SmartPtr<T>::SmartPtr(T *p)
{
    ptr = p;
    try
    {use_count = new int(1);
    }
    catch (...)
    {
        delete ptr; // 申请失败开释实在指针和援用计数的内存
        ptr = nullptr;
        delete use_count;
        use_count = nullptr;
    }
}
template <typename T>
SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig) // 复制构造函数
{
    use_count = orig.use_count;// 援用计数保留在一块内存,所有的 SmarPtr 对象的援用计数
    都指向这里
        this->ptr = orig.ptr;
    ++(*use_count); // 以后对象的援用计数加 1
}
template <typename T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs)
{
    // 重载 = 运算符,例如 SmartPtr<int>p,q; p=q; 这个语句中,首先给 q 指向的对象的援用计数加 1,因为 p 从新指向了 q 所指的对象,所以 p 须要先给原来的对象的援用计数减 1,如果减一后为 0,先开释掉 p 原来指向的内存,而后讲 q 指向的对象的援用计数加 1 后赋值给 p
    ++*(rhs.use_count);
    if ((--*(use_count)) == 0)
    {
        delete ptr;
        ptr = nullptr;
        delete use_count;
        use_count = nullptr;
    }
    ptr = rhs.ptr;
    *use_count = *(rhs.use_count);
    return *this;
}
template <typename T>
SmartPtr<T>::~SmartPtr()
{getcount();
    if (--(*use_count) == 0) //SmartPtr 的对象会在其生命周期完结的时候调用其析构函数,在析构函数中检测以后对象的援用计数是不是只有正在完结生命周期的这个 SmartPtr 援用,如果是,就开释掉,如果不是,就还有其余的 SmartPtr 援用以后对象,就期待其余的 SmartPtr 对象在其生命周期完结的时候调用析构函数开释掉
    {getcount();
        delete ptr;
        ptr = nullptr;
        delete use_count;
        use_count = nullptr;
    }
}
template <typename T>
T SmartPtr<T>::operator*()
{return *ptr;}
template <typename T>
T* SmartPtr<T>::operator->()
{return ptr;}
template <typename T>
T* SmartPtr<T>::operator+(int i)
{
    T *temp = ptr + i;
    return temp;
}

6 数组和指针

指针 数组
保留数据的地址 保留数据
指针的内容为为地址,从该地址拜访数据 间接拜访数据
通常用于动静的数据结构 通常用于固定数目且数据类型雷同的元素
通过 Malloc 分配内存,free 开释内存 隐式的调配和删除
通常指向匿名数据,操作匿名函数 本身即为数据名

7 野指针

野指针就是指向一个 已删除的对象 或者 未申请拜访受限内存区域 的指针

8 函数指针

8.1 定义

函数指针是 指向函数的指针 变量。
函数指针自身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
C 在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可援用其余类型变量一样,在这些概念上是大体一致的。

8.2 用处:

调用函数和做函数的参数,比方回调函数。

8.3 示例:

char * fun(char * p) {…}   // 函数 fun
char * (*pf)(char * p);    // 函数指针 pf
pf = fun;                  // 函数指针 pf 指向函数 fun
pf(p);                     // 通过函数指针 pf 调用函数 fun

9 fork 函数

Fork:创立一个和以后过程映像一样的 过程 能够通过 fork()零碎调用:

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

胜利调用 fork()会 创立一个新的过程 ,它简直与调用 fork() 的过程截然不同,这两个过程都会持续运行。在子过程中,胜利的 fork()调用会返回 0。在父过程中 fork()返回子过程的 pid。如果呈现谬误,fork()返回一个负值。

最常见的 fork()用法是创立一个新的过程,而后应用 exec()载入二进制映像,替换以后过程的映像。这种状况下,派生(fork)了新的过程,而这个子过程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的形式是很常见的。

在晚期的 Unix 零碎中,创立过程比拟原始。当调用 fork 时,内核会把所有的外部数据结构复制一份,复制过程的页表项,而后把父过程的地址空间中的内容逐页的复制到子过程的地址空间中。但从内核角度来说,逐页的复制形式是非常耗时的。古代的 Unix 零碎采取了更多的优化,例如 Linux,采纳了 写时复制 的办法,而不是对父过程空间过程整体复制。

10 析构函数

析构函数与构造函数对应,当对象完结其生命周期,如对象所在的函数已调用结束时,零碎会主动执行析构函数。

析构函数名也应与类名雷同,只是在函数名后面加一个位取反符~,例如 ~stud(),以区别于构造函数。它 不能带任何参数,也没有返回值 (包含 void 类型)。只能有一个析构函数, 不能重载

如果用户没有编写析构函数,编译系统会主动生成一个缺省的析构函数(即便自定义了析构函数,编译器也总是会为咱们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简略的类中没有用显式的析构函数。

如果一个类中有指针,且在应用的过程中动静的申请了内存,那么最好显示结构析构函数在销毁类之前,开释掉申请的内存空间,防止内存透露。

10.1 类析构程序

  1. 派生类自身的析构函数
  2. 对象成员析构函数
  3. 基类析构函数

因为析构函数没有参数,所以蕴含成员对象的类的析构函数模式上并无非凡之处。但在撤销该类对象的时候,会首先调用本人的析构函数,再调用成员对象的析构函数,调用秩序与初始化时的秩序相同。

11 虚函数和多态

多态的实现次要分为 动态多态 动静多态 动态多态次要是重载 ,在 编译的时候就曾经确定 动静多态是用虚函数 机制实现的,在 运行期间动静绑定 。例如:一个父类类型的指针指向一个子类对象时候,应用父类的指针去调用子类中 重写 了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中申明为加了 virtual 关键字的函数,在子类中重写时候不须要加 virtual 也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始局部是一个虚函数表的 指针 ,这个指针指向一个 虚函数表 ,表中放了虚函数的地址,理论的虚函数在代码段(.text) 中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的 虚函数表中的地址替换为从新写的函数地址。应用了虚函数,会减少拜访内存开销,升高效率。

12 析构函数与虚函数

析构函数必须是虚函数,因为将可能会被继承的父类的析构函数设置为虚函数,能够保障当咱们 new 一个子类,而后应用基类指针指向该子类对象,开释基类指针时能够开释掉子类的空间,避免内存透露。

C++ 默认的析构函数不是虚函数是因为虚函数须要额定的虚函数表和虚表指针,占用额定的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会节约内存。因而 C++ 默认的析构函数不是虚函数,而是只有当须要当作 父类 时,设置虚函数

13 动态函数和虚函数

动态函数在编译的时候就曾经确定运行机会,虚函数在运行的时候动静绑定。虚函数因为用了虚函数表机制,调用的时候会减少一次内存开销。

13.1 动态函数

用 static 润饰的函数,限定在本源码文件中应用,不能被本源码文件以外的代码文件调用。一般的函数,默认是 extern 的,也就是说,能够被其它代码文件调用该函数。

13.2 虚函数表

当一个类中蕴含被 virtual 关键字润饰的成员函数时,该成员函数就成为了一个 虚函数 。头一个含有虚函数的类所实例化进去的对象都领有同一个 虚函数表 ,在对象中含有一个 虚函数指针 *_vptr,该指针指向该类的虚函数表,虚函数表保留的是类中虚函数的地址(一个类可能有多个虚函数)。

13.3 虚函数作用

当一个子类继承了一个含有虚函数的基类,并重写了该基类中的一个虚函数,咱们就说这两个类形成多态。子类继承基类的同时,基类的虚函数表也被子类继承,不同的是被 子类重写的虚函数将会代替原来虚函数表中对应的基类的虚函数的地址。从而基类与子类调用同名的虚函数时,所调用的就不是同一个函数,从而体现了多态和虚函数表的作用。

13.4 动态函数与虚函数的区别

咱们晓得类的动态函数是没有 this 指针的,调用它时不须要创建对象,通过:类名::函数名(参数)的模式间接调用。动态函数只有 惟一的 一份,因而它的 地址是固定不变的 ,所以编译的时候凡是遇到调用该动态函数的时候就晓得调用的是哪一个函数,因而说 动态函数在编译的时候就曾经确定运行机会。 而虚函数则不然,看上面的代码:

class A
{
    public: 
    virtual void fun()
    {cout<<"i am A <<endl;}
}
class B: public A
{
    public:
    virtual  void  fun()
    {cout<<"I  am  B" <<endl;}
};
int main()
{
    A a ;
    B b;
    A*  pb = &b;
    pb->fun();
    return 0; 
}

类 A 与类 B 形成多态,创立了 A 类指针 pb 指向 B 类对象,当程序编译的时候只对语法等进行检测,该语句没有什么问题,然而编译器此时无奈确定调用的是哪一个 fun() 函数,因为类 A 类 B 中都含有 fun 函数,因而只能是在程序运行的时候通过 pb 指针查看对象的虚函数表(拜访虚函数表就是所谓的拜访内存)能力确定该函数的地址,即确定调用的是哪一个函数。这就解释了所说的“虚函数在运行的时候动静绑定。虚函数因为用了虚函数表机制,调用的时候会减少一次内存开销。

14 重载和笼罩

14.1 重载

两个函数名雷同,然而参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中。

14.2 重写

子类继承了父类,父类中的函数是虚函数,在子类中从新定义了这个虚函数,这种状况是重写,是一种同名笼罩。

15 在 main 函数前先运行的函数

1.test0__attribute((constructor))是 gcc 扩大,标记这个函数该当在 main 函数之前执行。同样有一个__attribute((destructor)),标记函数该当在程序完结之前(main 完结之后,或者调用了 exit 后)执行。

2.test1:全局 static 变量的初始化在程序初始阶段,先于 main 函数的执行。

#include <iostream> 
using namespace std;

__attribute((constructor)) void test0()
{printf("before main 0\n");
}

int test1()
{
    cout << "before main 1" << endl;
    return 54;
}

static int i = test1();
int main(int argc, char** argv) 
{
    cout << "main function." <<endl;
    return 0;
}

在 leetcode 里常常见到 static,在 main 之前敞开 cin 与 stdin 的同步来“放慢”速度的黑科技。

static int _ = []{cin.sync_with_stdio(false);
    return 0;
}();

16 内存治理

在 C++ 中,虚拟内存分为代码段、数据段、BSS 段、堆区、文件映射区以及栈区六局部。

1.栈 (stack):程序 主动调配,应用栈空间存储函数的返回地址、参数、局部变量、返回值。

2.堆(heap)

  • :调用malloc 在堆区动静分配内存,调用 free 来手动开释。堆是 操作系统所保护 的一块非凡内存,它提供了动态分配的性能。
  • 自在存储区 :由new 分配内存,用来 delete 手动开释。和堆相似,通过new 来申请的内存区域可称为自在存储区。

3.动态 / 全局区:在 C++ 外面没有辨别 bss 和 data。

  • bss 段 :存储 未初始化 的全局变量和动态变量(部分 + 全局),以及所有被 初始化为 0 的全局变量和动态变量,Block Started by Symbol。
  • data 段 :存储程序中 已初始化 的全局变量和动态变量。

4.代码区(code segment 或 text segment):

  • 代码段 :寄存函数体的二进制代码,text 段。
  • 常量区:只读数据,比方字符串常量,程序完结时由零碎开释。rodata 段,read only。

init 段:程序初始化入口代码,在 main() 之前运行。

17 常量 const

常量是固定值,在程序执行期间不会扭转。常量能够是任何的根本数据类型,可分为 int、float、char、string 和 bool。常量定义必须初始化。

17.1 存储区域

  1. 部分常量 ,寄存在 栈区
  2. static/ 全局常量 ,寄存在 动态 / 全局存储区
  3. 字面值常量 ,其值一望而知,寄存在 常量区

17.2 const 润饰成员函数

const 润饰的成员函数表明函数调用 不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上 const 限定,这样无论 const 对象还是一般对象都能够调用该函数

若同时定义了两个函数,一个带 const,一个不带,这相当于函数的 重载

18 代码解析

18.1 strcpy 和 strlen

strcpy 是字符串拷贝函数,原型:

char *strcpy(char* dest, const char *src);

从 src 逐字节拷贝 到 dest,直到遇到 ’\0’ 完结,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出破绽, 平安版本是 strncpy 函数。

strlen 函数是计算字符串长度的函数,返回从开始到 ’\0’ 之间的字符个数。

18.2 ++ i 和 i ++

++i 实现:

int& int::operator++(){
    *this +=1;return *this;}

i++ 实现:

const int int::operator(int){
    int oldValue = *this;++(*this);return oldValue;}

18.3 代码的区别

(1)字符串 123 保留在 常量区,const 原本是润饰 arr 指向的值,不能通过 arr 去批改,然而字符串“123”在常量区,原本就不能扭转,所以加不加 const 成果都一样:

const char * arr = "123";

(2)字符串 123 保留在常量区,这个 和 arr 指针指向的是同一个地位,同样不能通过 brr 去批改 ”123″ 的值:

char * brr = "123";

(3)这里 123 原本是在栈上的,然而编译器可能会做某些优化,将其放到常量区:

const char crr[] = "123";

(4)字符串 123 保留在 栈区,能够通过 drr 去批改:

char drr[] = "123";

19 编程题

19.1 点是在三角形内

给定三角形 ABC 和一点 P(x,y,z),判断点 P 是否在 ABC 内。

依据面积法,如果 P 在三角形 ABC 内,那么三角形 ABP 的面积 + 三角形 BCP 的面积 + 三角形 ACP 的面积应该等于三角形 ABC 的面积。

$$
S=(x_1y_2+x_2y_3+x_3y_1-x_1y_3-x_2y_1-x_3y_2)/2
$$

代码如下:

#include <iostream>
#include <math.h>
using namespace std;
#define ABS_FLOAT_0 0.0001
struct point_float
{
    float x;
    float y;
};

float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2)  // 计算三角形面积
{
    point_float AB, BC;
    AB.x = pt1.x - pt0.x;
    AB.y = pt1.y - pt0.y;
    BC.x = pt2.x - pt1.x;
    BC.y = pt2.y - pt1.y;
    return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}

bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D)  // 判断给定一点是否在三角形内或边上
{
    float SABC, SADB, SBDC, SADC;
    SABC = GetTriangleSquar(A, B, C);
    SADB = GetTriangleSquar(A, D, B);
    SBDC = GetTriangleSquar(B, D, C);
    SADC = GetTriangleSquar(A, D, C);
    float SumSuqar = SADB + SBDC + SADC;
    if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))
        return true;
    else
        return false;
}

19.2 判断一个数是二的倍数

判断一个数是不是二的倍数,即判断该数二进制末位是不是 0:

a % 2 == 0  
a & 0x0001 == 0  // 两种方法都可

19.3 一个数中有几个 1

能够间接逐位除十取余判断:

int fun(long x)
{
    int _count = 0;
    while(x)
    {if(x % 10 == 1)
            ++_count;
            x /= 10;
    }
    return _count;
}
int main()
{cout << fun(123321) << endl;
    return 0;
}

 
 

学习更多编程常识,请关注我的公众号:

代码的路

正文完
 0