C++基础知识

简介: C++基础知识

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。

CPP基本语言1.png

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;
}

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

代码的路

相关文章
|
6月前
|
存储 C++ 索引
C++ 字符串完全指南:学习基础知识到掌握高级应用技巧
C++的字符串使用`string`类处理,如`string greeting = &quot;Hello&quot;`。字符串连接可通过`+`或`append()`函数实现。访问字符使用索引,如`myString[0]`。`length()`或`size()`可获取长度。`getline()`用于读取整行输入。注意转义字符如`\\&quot;`用于在字符串中嵌入双引号。使用`cin`读取字符串时,空格会终止输入,而`getline()`能读取整行。
61 0
|
4月前
|
C语言 C++ 开发者
C++基础知识(一:命名空间的各种使用方法)
C++在C的基础上引入了更多的元素,例如类,类的私密性要比C中的结构体更加优秀,引用,重载,命名空间,以及STL库,模板编程和更多的函数,在面向对象的编程上更加高效。C语言的优势则是更加底层,编译速度会更快,在编写内核时大多数都是C语言去写。 在C++中,命名空间(Namespace)是一种组织代码的方式,主要用于解决全局变量、函数或类的命名冲突问题。命名空间提供了一种封装机制,允许开发者将相关的类、函数、变量等放在一个逻辑上封闭的区域中,这样相同的名字在不同的命名空间中可以共存,而不会相互干扰。
|
4月前
|
C++
C++基础知识(二:引用和new delete)
引用是C++中的一种复合类型,它是某个已存在变量的别名,也就是说引用不是独立的实体,它只是为已存在的变量取了一个新名字。一旦引用被初始化为某个变量,就不能改变引用到另一个变量。引用的主要用途包括函数参数传递、操作符重载等,它可以避免复制大对象的开销,并且使得代码更加直观易读。
|
4月前
|
算法 编译器 C++
C++基础知识(三:哑元和内联函数和函数重载)
在C++编程中,"哑元"这个术语虽然不常用,但可以理解为在函数定义或调用中使用的没有实际功能、仅作为占位符的参数。这种做法多见于模板编程或者为了匹配函数签名等场景。例如,在实现某些通用算法时,可能需要一个特定数量的参数来满足编译器要求,即使在特定情况下某些参数并不参与计算,这些参数就可以被视为哑元。
|
4月前
|
C++
C++基础知识(四:类的学习)
类指的就是对同一类对象,把所有的属性都封装起来,你也可以把类看成一个高级版的结构体。
|
4月前
|
自然语言处理 程序员 C++
C++基础知识(五:运算符重载)
运算符重载是C++中的一项强大特性,它允许程序员为自定义类型(如类或结构体)重新定义标准运算符的行为,使得这些运算符能够适用于自定义类型的操作。这样做可以增强代码的可读性和表达力,使得代码更接近自然语言,同时保持了面向对象编程的封装性。
|
4月前
|
存储 编译器 C++
C++基础知识(六:继承)
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。
|
4月前
|
存储 编译器 C++
C++基础知识(七:多态)
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。
|
4月前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。
|
4月前
|
算法 前端开发 C++
C++基础知识(八:STL标准库 deque )
deque在C++的STL(Standard Template Library)中是一个非常强大的容器,它的全称是“Double-Ended Queue”,即双端队列。deque结合了数组和链表的优点,提供了在两端进行高效插入和删除操作的能力,同时保持了随机访问的特性。