C++基础总结

一、基本的代码含义

1、程序的第一行#include <iostream>是预处理器指令,告诉 C++ 编译器在实际编译之前要包含iostream 文件;

2、using namespace std表示我们可以使用标准库中对象和变量的名称;

3、int main()是主函数,程序从这里开始执行;

4、/*...*/将会被编译器忽略,这里放置程序的注释内容。它们被称为程序的注释;

5、return 0;终止 main() 函数,并返回值 0

二、定义常量的两种方式

1、const 定义常量

const type variable = value;

特点:不能直接修改,但是可以间接修改可以通过直接修改内存进行修改可以通过指针进行修改;

2、#define 定义常量

#define  <宏名/标识符>  <字符串>

特点:不能通过指针进行修改,也不能通过内存进行修改,相当于真正意义的常量;

一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换;

三、C++存储区

C++存储区分为以下几个区域:

1、代码区:存放CPU执行的机器指令,代码区是可共享,并且是只读的;

2、数据区:存放已初始化的全局变量、静态变量(全局和局部)、常量数据;

3、BBS区:存放的是未初始化的全局变量和静态变量;

4、栈区:由编译器自动分配释放,存放函数的参数值、返回值和局部变量,在程序运行过程中实时分配和释放,栈区由操作系统自动管理,无须程序员手动管理;

5、堆区:堆是由malloc()函数分配的内存块,使用free()函数来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏;

四、C++存储类型

1、auto类型

注意:在C++11及以后,auto不再是存储类型,而是一种自动推断变量的类型;

例子如下:

auto d=4.38;      //double
auto s("hello");  //const char*
auto z = new auto(10); // int*
auto x1 = 5, x2 = 5.0, x3='c';//错误,必须是初始化为同一类型

注意:C++11的auto关键字时有一个限定条件,那就是必须给申明的变量赋予一个初始值,否则编译器在编译阶段将会报错;

2、extern存储类型

extern用来声明在当前文件中引用在当前项目中的其它文件中定义的全局变量。如果全局变量未被初始化,那么将被存在BBS区中,且在编译时,自动将其值赋值为0,如果已经被初始化,那么就被存在数据区中。全局变量,不管是否被初始化,其生命周期都是整个程序运行过程中,为了节省内存空间,在当前文件中使用extern来声明其它文件中定义的全局变量时,就不会再为其分配内存空间;

这里首先要了解一下声明和定义的区别:

//A.h
int i;                //声明并定义i
int j = 1;            //声明并定义j
double max(double d1,double d2){} //定义

//A.cpp
extern int j;          //声明j而非定义(此时j的值为1)
extern int i = 2;     //定义i而非声明,会报错,多重定义
int j;                //错误,重定义,注意这里的j是在全局范围内声明
extern double max(double d1,double d2); //声明

说明:

  • 在头文件中,定义并声明i时,会默认初始化为0;

  • 如果两个文件中有同名的变量,则会产生重定义错误,需要引用extern来避免重定义;

  • 凡是没有带extern的声明同时也都是定义;

  • 对函数而言,带有{}是定义,否则是声明;

拓展:

extern "C"经常会出现在C+=代码中,作用是什么呢?

参考文章:https://www.jianshu.com/p/5d2eeeb93590

作用:告诉C++编译器,下面的代码按照C的方式进行编译,说白了,不要对这些函数进行名字重整(function name mangling)。通常在C++程序中使用C函数或者模块时,需要用到这个功能;

因为C++为了支持函数重载,会将编译后的函数名进行重整,会导致连接不到C的函数;

3、register存储类型

声明为register的变量在由内存调入到CPU寄存器后,则常驻在CPU的寄存器中,因此访问register变量将在很大程度上提高效率,因为省去了变量由内存调入到寄存器过程中的好几个指令周期;

代码案例:

#include <iostream>
using namespace std;  
int main(void)  
{  
    register int i,sum=0;  
    for(i=0;i<100;i++)  
        sum=sum+1;  
    cout << sum  << endl;
    return 0;  
}  

说明:这个在实际工程中也用的不多,暂时还没测试其效率是否有提升;

4、static存储类型

被声明为静态类型的变量,无论是全局的还是局部的,都存储在数据区中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{}内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。静态变量只能够初始化一次;

代码案例:

int test(int a)
{
	auto int c=0;  
    static int b=3;
    c++;
    b++;
    cout << "c: " << c << "b: " << b << endl;
}

int main()
{
	int i;
	for(i = 0; i < 100; i++){
		test();
	}
	return 0;
}

说明:上述代码中,变量c每次都会进行初始化,所以每次打印都为1;而变量b指挥初始化一次,所以每次都会进行递增;

五、C++智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用;智能指针的作用是管理一个指针。因为可能申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间;

  • share_ptr:

shared_ptr采用引用计数的方式管理所指向的对象。当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1。当shared_ptr离开作用域时,引用计数减1。当引用计数为0时,释放所管理的内存;

  • weak_ptr:

weak_ptr一般和shared_ptr配合使用。它可以指向shared_ptr所指向的对象,但是却不增加对象的引用计数。当出现weak_ptr所指向的对象实际上已经被释放了的情况。weak_ptr有一个lock函数,尝试取回一个指向对象的shared_ptr

  • unique_ptr:

unique_ptr对于所指向的对象, 指向的对象是独占的。不可以对unique_ptr进行拷贝、赋值等操作,但是可以通过release函数在unique_ptr之间转移控制权;

说明:智能指针还没有实际用过,对于其中的一些坑后续再做总结;

六、指针函数和函数指针

1、指针函数

简单理解就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针;

代码示例:

int *myfun(int x,int y);

说明:

也就是在函数前加入一个*号;其返回值必须通过一个同类型指针来接收其参数;

也可以将其返回值定义为 void*类型,调用时强制转换返回值为想要的类型;

2、函数指针

本质是一个指针变量,该指针指向这个函数。函数指针就是指向函数的指针;

代码示例:

int (*myfun)(int x,int y);

说明:

如果想把一个函数的地址传递给函数指针变量,可以直接将函数名赋值即可;(函数标识符就代表了地址)

这里的函数指针作用很大,可以将函数作为一个参数传入另一个函数;

二者的区别:

① 定义上的不同:

指针函数本身是一个函数,其返回值为指针;

函数指针本身是一个指针,其指向一个函数;

②用法不同:

一个是函数,一个是变量;

七、C++中的typedef关键字

使用typedef关键字为指定类型取一个别名,可以为char*取一个别名为pStr

代码示例:

typedef char* pStr;

当然typedef也可以用在别的类型上,甚至是结构体;

typedef和#define的区别:

1、typedef仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如可以定义 1 为 ONE;

2、typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的;

3、typedef是别名定义,而#define知识单纯的替换字符;

八、C++中的引用变量

引用变量是在C++中新的复合类型。引用变量被定义为一个变量的别名,即引用变量和其指向的变量代表同一个值,指向同一个存储单元,并且引用变量自从赋值起就已知跟着这个变量,不会再发生改变,也就是一个变量两个名字,所以更改其中的任何一个这个变量都会发生改变;

这里就涉及一个运算符重载:&符号可以取变量的地址,同时它的另一个含义就是用来声明引用变量;

代码示例:

int i;
int & r = i;

引用变量和指针的区别?

1、不存在空引用,引用必须连接到一块合法的内存;

2、引用被初始化给一个对象,就不能绑定到别的对象,而指针可以在任何时候指向另一个对象;

3、引用必须在创建时初始化,指针可以在任意时候初始化;

引用作为函数参数传递:

代码示例:

#include <iostream>
using namespace std;
int main()
{
      void swap(int & ,int&);
      int i =3,j = 5;
      swap(i,j);
     cout<<"i="<<i<<endl<<"j="<<j<<endl;
      getchar();
      return 0;
}
void swap(int &a,int &b)
{
    int temp;
    temp = a;
    a =b;
    b= temp;
}

说明:C++引用作为参数传递,直接传入地址,不再开辟新的复制空间,C语言中没有引用,只有指针。

九、C++预处理命令

定义:以#作为开头的代码行称为预处理命令,预处理是在编译之前进行的处理;

下图为一些预处理命令表格:

在这里插入图片描述

1、宏定义

宏定义又称为宏代换、宏替换,简称“宏”;

使用示例:

#define PI 3.1415926

注意:宏名一般为大写,也可以创建带参数的宏,因为宏定义是单纯的字符替换;

拓展:

C++中有一些预定义宏,如下图所示:

在这里插入图片描述

可以直接使用cout进行打印输出;

2、#include

这里只需要注意一点,<文件名>称为标准方式,系统到头文件目录查找文件,"文件名"则先在当前目录查找,而后到头文件目录查找;

3、条件编译

这里可以用于一些标识符的定义,也就是控制需要执行的代码段;

代码示例:

#ifdef 标识符
{程序段1}
#else
{程序段2}
#endif

十、C++访问修饰符

常见的访问修饰符也就三种:public、private、protected;

1、public:修饰成员在任意地方都可以访问。公有成员在程序中类的外部是可访问的。可以不使用任何成员函数来设置和获取公有变量的值;

2、修饰的成员只能够在类中或者友元函数中可以访问。私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的;

3、修饰的成员可以在类中的函数、子类函数及友元函数中访问。保护成员变量或函数与私有成员类似,但有一点不同,保护成员在派生类(即子类)中是可访问的;

注意点:

private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;

protected 成员可以被派生类访问;

十一、C++析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。删除对象时,自动被调用,用来释放对象占用的空间;

注意:析构函数不会返回任何值,也不能带有任何参数;

拓展:

当定义了一个类后,主要有三种方式创建对象:

1、栈中分配内存:

Test A(1);

2、栈中分配内存(等同第一种):

Test A = Test(1);

3、堆中分配内存:

Test *A = new Test(1);

注意:

栈中分配内存,在栈中内存由系统自动的去分配和释放,而使用new创建的指针对象是在堆中分配内存,当不需要该对象时,需要我们手动的去释放,否则会造成内存泄漏。栈中对象的释放顺序,是后定义的先释;

拓展:

这里还介绍了一种拷贝析构函数,暂时没弄懂用途:https://www.cjavapy.com/article/1822/

十二、C++友元函数和友元类

简介:

C++需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。 友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员;

1、友元函数

友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend

简单示例:

//先定义一个类
class MyClass
{
  private:
    int age;
  public:
    //用来改变age
    void setage(int i);
    //友元函数,参数是MyClass对象
    friend void myFun(MyClass obj);
};
//成员函数,要加::
void MyClass::setage(int i)
{
   age = i;
}
//正常的普通函数而已
void myFun(MyClass obj)
{
  cout<<obj.age<<endl;
  obj.age = 998;
  cout<<obj.age<<endl;
}
int main()
{
  MyClass f1;
  myFun(f1);
  f1.setage(1000);
  return 0;
}

注意:

  • 友元函数的声明可以放在类的私有部分,也可以放在公有部分,并没有区别;
  • 一个函数可以是多个类的友元函数,只需要在各个类中分别声明;
  • 友元函数的调用与一般的函数调用一致;

2、友元类

简介:

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员);

需要类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类;

具体实例代码:

#include <iostream>
using namespace std;
class Box
{
    double width;
public:
    friend void printWidth(Box box);
    friend class BigBox;
    void setWidth(double wid);
};
class BigBox
{
public :
    void Print(int width, Box &box)
    {
        // BigBox是Box的友元类,它可以直接访问Box类的任何成员
        box.setWidth(width);
        cout << "Width of box : " << box.width << endl;
    }
};
// 成员函数定义
void Box::setWidth(double wid)
{
    width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
    /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
    cout << "Width of box : " << box.width << endl;
}
// 程序的主函数
int main()
{
    Box box;
    BigBox big;
    // 使用成员函数设置宽度
    box.setWidth(10.0);
    // 使用友元函数输出宽度
    printWidth(box);
    // 使用友元类中的方法设置宽度
    big.Print(20, box);
    getchar();
    return 0;
}

注意:

  • 友元类必须是程序中已经定义的一个类;
  • 友元关系不能被继承;
  • 友元关系是单向的,不具有交换性,比如B是A的友元类,则A不一定是B的友元类;

十三、C++内联函数

简介:

内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。内联函数可减少cpu的系统开销,并且程序的整体速度将加快,但当内联函数很大时,会有相反的作用,因此一般比较小的函数才使用内联函数;

代码案例:

一个函数定义为内联函数,需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符;(在类中定义的函数都是内联函数)

inline int Max(int x, int y)
{
   return (x > y)? x : y;
}
// 程序的主函数
int main( )
{
   cout << "Max (20,10): " << Max(20,10) << endl;
   cout << "Max (0,200): " << Max(0,200) << endl;
   cout << "Max (100,1010): " << Max(100,1010) << endl;
   return 0;
}

内联函数和普通函数的区别:

1、编译结果不同

内联函数(也称为在线函数和编译期展开函数)是一种编程语言结构,用来建议编译器对一些函数进行内联拓展,也就是说建议编译器将指定函数体插入并取代每一处调用该函数的地方(上下文),普通函数则会编译成单独的模块;

2、编译的时间不同

内联函数的使用会大大增加编译时间,因为每个调用内联函数的地方都要替换成函数体;

3、运行效率不同

使用内联函数必须在程序的占用空间和执行效率之间进行权衡,过多的函数体内联将造成较大的存储开支;

拓展:

函数调用的逻辑是如何的?

调用函数实际上将程序执行顺序转移到函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率;

十四、C++中this指针

简介:

类的成员函数可以访问类的数据,一般类成员和函数操作都是通过对象,每个对象都拥有一个指针:this 指针,通过this指针来访问自己的地址。this 指针并不是对象的一部分,this 指针所占的内存大小是不会反应在sizeof操作符上的。this 指针的类型取决于使用this指针的成员函数类型以及对象类型。this只能在成员函数中使用。全局函数,静态函数都不能使用 thisthis在成员函数的开始执行前构造的,在成员的执行结束后清除;

代码案例:

class Person
{
  int sno;
  string sname;
  int age;
  int grade;
public:
  Person(int s=0,string n="",int a=0,int g=0)
  {
    sno=s;
    sname=n;
    age=a;
    grade=g;
  }
  void Setsname(int sn)   //使用this指针进行赋值
  {
    this->sname=sn;
  }
  int  Setage(int a)
  {
    this->age=a;
    return (*this).age; //使用this指针返回该对象的年龄
  }
  void print()
  {
    cout<<"sname = "<<this->sname<<endl;  //显式this指针通过箭头操作符访问
    cout<<"sno   = "<<sno<<endl;//隐式使用this指针打印
    cout<<"age   = "<<(*this).age<<endl;//显式使用this指针通过远点操作符
    cout<<"grade = "<<this->grade<<endl<<endl;
  }

注意:this指针只能在类成员函数中使用;

十五、类的静态成员变量

使用static关键字来把类成员变量定义为静态的。当我们声明类的成员为静态时,即使多个类的对象,静态成员都只有一个副本;

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

代码案例:

class Area
{
   public:
      static int objectCount;
      // 构造函数定义
      Area()
      {
         objectCount++;
      }
};
// 初始化类 Area 的静态成员
int Area::objectCount = 0;
int main(void)
{
   Area Area1();    // 声明 Area1
   Area Area2();    // 声明 Area2
   // 输出对象的总数
   cout << "Total objects: " << Area::objectCount << endl;		// 输出结果为2,可以认为objectCount只初始化一次
   return 0;
}

拓展:静态成员函数即只要使用类名加范围解析运算符 :: 就可以访问,用的不多,在这就不作说明了;

十六、C++虚函数和纯虚函数

首先需要了解一下多态这个概念:

随着继承的普遍应用,多态就是派生类对于基类中函数的复用,并且可以对其进行修改,增加了代码的复用性;

接着要明确一个概念:

定义一个函数为虚函数,不代表函数为不被实现的函数,是为了允许用基类的指针来调用子类的这个函数;

定义一个函数为纯虚函数,才代表函数没有被实现,是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数;

1、虚函数

代码案例:

class A
{
public:
    virtual void foo()
    {
        cout<<"A::foo() is called"<<endl;
    }
};
class B:public A
{
public:
    void foo()
    {
        cout<<"B::foo() is called"<<endl;
    }
};
int main(void)
{
    A *a = new B();
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

说明:虚函数就虚在所谓"推迟联编"或者"动态联编"上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为"虚"函数;如果说这里不是虚函数,那么用指针指向a,调用的就为a的函数;

2、纯虚函数

定义:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0

作用:

将函数定义为纯虚函数,则编译器要求派生类必须重写函数以实现多态性,同时含有纯虚函数的类称为抽象类,不能生成对象;

声明了纯虚函数的类是一个抽象类,用户不能创建类的实例,只能创建它的派生类的实例;

特点:

必须在继承类中重新声明函数,并且在抽象类中往往也没有定义;

拓展:

抽象类的定义是存在纯虚函数的即为抽象类;

但类中只存在纯虚函数的时候才能称为接口;

接口是一种特殊的抽象类;

总结:

  • 纯虚函数一定没有定义,是用于规范派生类的行为;虚函数则必须定义,不定义将报错;
  • 对于虚函数来说,父类和子类都有各自的版本,由多态调用的时候动态绑定;
  • 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数;
  • 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的;

参考文章:

https://www.runoob.com/w3cnote/cpp-virtual-functions.html

https://zhuanlan.zhihu.com/p/37331092

十七、C++模板

简介:

C++ 模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码;

1、函数模板

定义形式:

Template <class或者也可以用typename T>
返回类型 函数名(形参表)
{//函数定义体 }

注意:template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型;

代码案例:

template <class T>
T min(T x,T y)
{ return(x<y)?x:y;}
int main( )
{
     int n1=2,n2=10;
     double d1=1.5,d2=5.6;
     cout<< "较小整数:"<<min(n1,n2)<<endl;
     cout<< "较小浮点数:"<<min(d1,d2)<<endl;
     return 0;
}

2、类模板

定义形式:

Template < class或者也可以用typename T >
class 类名{
//类定义......
};

这个还不够了解,在后续会进行补充;

一些小的知识点记录

1、创建char*变量时候报错;

char* pstr = "hello world.";

qr3w11.png

解决方法:

参考文章:https://blog.csdn.net/whatday/article/details/106267398

实际上是一种类型匹配错误,可能和编译器版本有关;

下面两种修改方式都可以;

char const *pstr = "hello world.";
char *pstr = (char*)"hello world.";

2、对于c_str()的作用;

有时候需要将string类型转换为char*类型,转换代码如下:

const char* pszOutput = strOutput.c_str();

3、C++11的右值引用和std::move

参考文章:https://zhuanlan.zhihu.com/p/335994370

std::move:目的是将左值强制转换为右值;

作为函数形参传入时,右值引用更加灵活,const左值引用虽然也能做到接收左右值,但无法修改,具有一定局限性;

void f(const int& n) {
    n += 1; // 编译失败,const左值引用不能修改指向变量
}

void f2(int && n) {
    n += 1; // ok
}

int main() {
    f(5);
    f2(5);
}

说明:当然这种传参还不是右值引用的实际用途,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能

例如说vector::push_back使用std::move提高性能,具体案例可以参考文章中的介绍;

4、标准库

一些标准库的介绍:https://www.cjavapy.com/article/1840/

参考

https://www.cjavapy.com/category/65/

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐