sizeof()是一个运算符,不是一个函数

看程序效率的快慢,可以直接看其汇编语言程序的多少

扩展名:

c语言:cc++:cpp

Java:先有类,再有方法

c++完全兼容c语法

getchar()等待键盘输入(如果敲回车,就会读取键盘输入)

函数重载(overload)

  1. c语言不支持函数重载

  2. 两个函数的函数名完全相同,但是函数参数类型,个数,顺序不同
    返回值类型不同,不能构成重载

  3. 隐式转换(小转大)也可在函数参数类型中适用,但要注意隐式转换有可能产生二义性。

本质:

采用了name mangling 或者叫 name decoration技术

 C++编译器默认会对符号(比如函数名)进行改编、修饰​ 重载时会生成多个不同的函数名,不同编译器(MSVC、g++)有不同的生成规则

IDA pro 逆向工程 软件


Debug模式:很多调试信息Release模式:去除调试信息,生成的可执行文件比较精简、高效,会默认对函数进行优化

visual stdio 2019可选择不对其进行优化

默认参数

C++允许函数设置默认参数,在调用时可以根据情况省略实参。规则如下:

​ · 默认参数只能按照右到左的顺序

​ · 如果函数同时有声明、实现,默认参数只能放在函数声明中

​ · 默认参数的值可以是常量、全局符号(全局变量、函数名)

void(*p)(int) = test; //此语句的作用就是更换test函数的名称,其中void指test函数的返回类型,int指test函数中参数类型,p为将要命名的函数名称。
p(10);
#include <iostream>
using namespace std;void func(int v1, void(*p)(int)=test)
{p(v1);
}void test(int a)
{cout << "test(int)-" << a << endl;
}int main()
{func(30);func(10, test);return 0;
}

如果函数的实参经常是一个值,可以考虑使用默认参数

函数重载 、默认参数可能会产生冲突、二义性(建议优先选择使用默认参数)
例:

void display(int a,int b=20)
{cout<<"a is"<<a<<endl;
}void display(int a)
{cout<<"a is"<<a<<endl;
}int main()
{display(10);//该语句会产生二义性return 0;
}

默认参数的本质

 自动补充参数

extern “C”

被extern "C"修饰的代码会按照c语言的方式编译

#include <iostream>
using namespace std;extern "C" void func()
{}extern "C" void func(int v)
{}int main()
{return 0;
}

写法

extern "C"
{void func(){}void func(int v){}
}extern "C" void func()
{}

:以上代码报错,因为C语言中不支持函数重载

如果函数同时有声明和实现,要让函数声明被extern “C”修饰,函数实现可以不修饰。

#include <iostream>
using namespace std;extern "C" void func();int mian()
{return 0;
}extern "C" void func()
{}

**如果相同的函数以不同的语言编译,所代表的函数不相同**#include <iostream>
using namespace std;void func();
extern "C" void func(int v);//除去参数V报错原因与重载无关,是二义性int main()
{func();func(10);return 0;
}void func()
{cout << "func()" << endl;
}void func(int v)
{cout << "func(int v)-"<<v << endl;
}

externC2-C、C++混合开发

由于C、C++编译规则的不同,再C、C++混合开发时,可能会经常出现一下操作

extern 无法在C语言中使用

每个C++文件之前都隐含着一个定义 #define __cplusplus

可根据这个特点来使头文件在不同的语言文件中判断是否添加extern ”C”

如果定义了 __cplusplus这个,则extern "C"参与编译

math.h头文件的定义

#ifdef __cplusplus
extern "C"{
#endifint sum(int v1, int v2);int delta(int v2, int v3);int divide(int v1, int v2);#ifdef __cplusplus
}
#endif

C++可以不导入头文件,但必须要写声明,头文件其实就是声明

重复包含头文件,浪费编译器工作时间

为了防止重复包含头文件**#ifndef…#endif**

#ifndef 1            //为了防止重复定义头文件#ifdef __cplusplus
extern "C" {
#endifint sum(int v1, int v2);int delta(int v2, int v3);int divide(int v1, int v2);#ifdef __cplusplus
}
#endif#endif // !1

注意:防止重复包含头文件的宏都是唯一的,一般使用__文件名

pragma once

我们经常使用#ifndef、#define、#endif来防止头文件的内容被重复包含

#pragma once可以防止整个文件的内容被重复包含

使用:放在头文件中最前面

区别:

1.#ifndef、#define、#endif受C\C++标准的支持,不受编译器的任何限制

2.有些编译器不支持#pragma once(较老编译器不支持,如GCC3.4版本之前),兼容性不够好

3.#ifndef、#define、#endif可以针对一个文件中的部分代码,而#pragma once只能针对整个文件

内联函数(inline function)

使用inline修饰的函数的声明或者实现,可以使其变为内联函数**

将函数展开为函数体代码

#include <iostream>
using namespace std;inline int sum(int v1, int v2);  //将函数调用展开为函数体代码inline void func()
{cout << "func()" << endl;cout << "func()1" << endl;
}int main()
{func();/*这里的func()相当于cout << "func()" <<endl;*/int c = sum(10, 20);/*这里相当于int c = 10 + 20;*/cout << c << endl;return 0;
}inline int sum(int v1, int v2)
{return v1 + v2;
}

意义:

函数调用需要开辟栈空间,回收栈空间,内联函数则不存在栈空间的开辟,不需要回收内存

什么时候使用内联函数

​ 适用于函数体积小

​ 经常调用的函数

特点

​ 编译器会将函数调用直接展开为函数体代码

​ 可以减少函数调用的开销

​ 会增大代码体积

注意

​ 尽量不要内联超过10行代码的函数

​ 有些函数即使声明为inline,也不一定会被编译器内联,比如递归函数

内联函数-----宏

#define add(v1,v2) v1+v2int main()
{cout << add(10, 20) << endl;return 0;
}

对比:

​ 内联函数和宏,都可以减少函数调用的开销

​ 对比宏,内联函数多了语法检测和函数特性

#define add(v) v+vinlinde int add1(int v)
{return v+v;
}int main()
{int a=10;cout<<add(++a)<<endl;//结果为24,相当于cout << ++a + ++a <<endl; cout<< add1(++a)<<endl;//结果为22 return 0;}

表达式

C++有些表达式可以被赋值

int a=1;
int b=2;
(a=b)=4;
//这里a=4,b=2

const

​ const 是常量的意思,被其修饰的变量不可修改

​ 如果修饰的是类、结构体(的指针),其成员也不可修改

#include <iostream>
using namespace std;struct Data
{int year;int month;int day;
};int main()
{const int age = 10;const Data d = { 1200, 23, 2 };d.year = 15;cout << d.year << endl;//输出错误return 0;
}

以下5个指针是什么含义?

int age=10;const int *p1=&age;             //*p1是常量,p1可以改变
int const *p2=&age;                //与p1没区别
int * const p3=&age;           //p3是常量,不能改变,*p3可以改变
const int *  const p4=&age;        //与p5没区别 p4,*p4都是常量
int const *const p5=&age;

const修饰的是其右边的内容

struct Student {int age;};Student stu1={10};
Student stu2={20};const Student *pStu1=&stu1;  //修饰 *pStu1
*pStu1 = stu2;             //报错 直接访问
(*pStu1).age=30;           //报错 间接访问
pStu1->age=30;              //报错 间接访问
pStu1 = &stu2;             //正确Student * const pStu2 = &stu2; //修饰pStu2
*pStu2=stu1;               //正确
(*pStu2).age=30;           //正确
pStu2->age=30;              //正确
pStu2=&stu1;               //报错

引用(Reference)

在c语言中,使用指针间接修改某个变量的值。

 int age = 10;int height = 20;//定义了一个age的引用int& ref = age;int& ref1=ref;int& ref2=ref1;ref = 20;age += 30;age = height;age = 11;cout << height << endl;

注意

引用相当于是变量的别名(基本数据类型、枚举、结构体、类、指针、数组等,都可以由引用)

对引用做计算,就是对引用所指向的变量做计算

在定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终”

可以利用引用初始化另一个引用,相当于某个变量的多个别名

不存在【引用的引用、指向引用的指针、引用数组】

引用的价值

比指针更安全、函数返回值可以被赋值

void swap(int  &v1, int  &v2)
{int tmp = v1;v1 = v2;v2 = tmp;
}int main()
{int a = 10;int b = 20;swap(a, b);cout << "a=" << a << ",b=" << b << endl;return 0;
}

参数的引用是栈里的

引用的本质:

​ 引用的本质就是指针,只是编译器削弱了它的功能,所及引用就是弱化的指针

int age=10;//*p就是age的别名
int *p=&age;
*p=30;//ref就是age的别名
int &ref=age;
ref=40;

指针的大小和环境有关(X86,X64)

ASLR 让应用的起始地址是随机的

汇编

汇编语言的种类

​ 8086汇编(16bit)

​ X86汇编(32bit)

X64汇编(64bit)

​ ARM汇编(嵌入式、移动设备)

X64汇编根据编译器的不同,有两种书写格式

Intel

​ AT&T

汇编语言不区分大小写

学习汇编语言2大知识点:

1.汇编指令

2.寄存器

寄存器

​ 通常,CPU会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算。

64bit:

通用寄存器:RAX \ RBX \ RCX \ RDX;

寄存器大小和环境有关,64位为8个字节

32bit:

通用寄存器:EAX \ EBX \ECX \EDX;

16bit:

通用寄存器:AX \ BX \ CX \DX

一般规律:

R开头的寄存器是64bit的,占8个字节

**imm:**立即数

内联汇编

#include <iostream>
using namespace std;int main()
{int a = 10;__asm{mov eax,a}return 0;
}

mov dest,src

将src的内容赋值给dest,类似于dest=src

[地址值]

​ 1.中括号[]里面放的都是内存地址

2.word 是2字节,dword是4字节(double word),qword是8字节      (quard   word)

call 函数地址

add op1,op2

​ 类似于op1=op1+op2

sub op1,op2

类似于op1=op1-op2

inc op

​ 自增,类似于op=op+1

dec op

​ 自减,类似于op=op-1

jmp 内存地址

​ 跳转到某个内存地址去执行代码

​ j开头的一般都是跳转,大多数是带条件的跳转,一般跟test、cmp等指令配合使用

指针的汇编特征

lea      eax,[ebp-0Ch]
mov     dword ptr [ebp-18h],eax

引用的汇编特征

与指针相同

引用的补充

//结构体的引用
#include <iostream>
using namespace std;struct Data
{int year;int month;int day;
};int main()
{Data d = { 2021,07,20 };Data& ref = d;d.day = 2013;cout << d.day << endl;return 0;
}
//指针的引用int age = 10;int* p = &age;int* &ref = p;//引用的 数据类型是int**ref = 30;int height=30;ref=&height;//更改ref的地址

数组名array其实是数组的地址,也是数组首元素的地址

数组名array可以看做是指向数组首元素的指针(int *)

//数组的引用int array[]={1,2,3};
//法一:
int (&ref)[3]=array;
ref[0]=10;
//法二:
int * const &ref = array;

指针数组

数组里面可以存放3个int*

​ *int p;

​ *int arr1[3]={p,p,p}

用于指向数组的指针

​ 指向存放3个整数的数组

int (*arr2)[3];

常引用(const Reference)

引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用

const必须写在&符号的左边,才算是常引用


int age=10;
const int &ref=age;
ref=30; //这时就不能更改其值,

意义:

​ 只读,阻止其他函数修改所引用的值

 int func(const int &a);
const int &ref=age;//与下面相同
int const &ref=age;//ref不可以修改指向,且不可以通过ref间接修改所指向的变量int &const ref=age;//ref不能修改指向,但是可以通过ref间接修改所指向的变量  **没有意义**
相当于int * const ref=age;

const引用的特点

​ 可以指向临时数据(常量、表达式、函数返回值等)

​ 可以指向不同类型的数据

int age = 10;int a=1;
int b=2;const int &ref1 = a + b;
const int &ref = 30;
const double *ref2 = age;

​ 作为函数参数时(此规则也适用于const指针

​ 可以接受const和非const实参(非const引用,只能接受非const实参)

​ 可以跟非const引用构成重载

int sum(const int &v1,const int &v2)
{return v1 + v2;
}int sum(int &v1,int &v2)
{return v1 + v2;
}int main()
{int a = 10;int b = 20;sum(a,b);const int c = 10;const int d = 20;sum(c,d);sum(10,20);//重要作用}

当常引用指向了不同类型的数据时,就会产生临时变量,即引用指向的并不是初始化时的那个变量

int age = 10;
const int &rAge = age;
age = 30;cout << " age is " << age <<endl;     30
cout << " rAge is " << rAge <<endl;   30
int age = 10;
const double &rAge = age;
age = 30;cout << " age is " << age <<endl;     30
cout << " rAge is " << rAge <<endl;   10

面向对象

类和对象

​ C++可以使用struct、class来定义一个类

使用struct定义类

#include <iostream>
using namespace std;struct  Person
{int age; //成员变量void fun()//成员函数(方法){cout << "Person :: run()"<<age << endl;}
};int main()
{//利用类创建对象Person person;    //这个对象的内存在栈空间person.age = 10;person.fun();return 0;
}

struct 和 class的区别

​ struct的默认成员权限是public

​ class的默认诚邀您权限是private

命名规范

1.全局变量:g_
2.成员变量:m_
3.静态变量:s_
4.常量:c_

通过指针间接访问person对象

#include <iostream>
using namespace std;struct  Person
{int age; //成员变量void fun()//成员函数(方法){cout << "Person :: run()"<<age << endl;}
};int main()
{Person* p = &person;p->age = 20;p->fun();return 0;
}

上面代码中的person 对象、p指针的内存都是在函数的栈空间,自动分配和回收的

64位环境下,person占用4个字节(Person类中只有一个int变量),p占用8个字节(64位环境下,地址占用8个字节)

实际开发中,使用class比较多

this this就是指针

​ this指针存储着函数调用者的地址

​ this指向了函数的调用者

#include <iostream>
using namespace std;struct Person
{int m_age;void run(){cout << "Person::run()" << this->m_age << endl;//this是隐式参数}
};int main()
{Person person;person.m_age = 21;person.run();return 0;
}

作用:可以使不同的对象,调用同一个函数。

指针访问的本质

lea      eax,[ebp-14h]
mov     dword ptr [ebp-20h],eaxmov      eax,dword ptr[ebp-20h]
mov     dword ptr [eax],0Ahmov      eax,dword ptr [ebp-20h]
mov     dword ptr [eax],0Ah

原理:如何利用指针间接访问所指向对象的成员变量?

​ 1.从指针中取出对象的地址

​ 2.利用对象的地址 + 成员变量的偏移量计算出成员变量的地址

​ 3.根据成员变量的地址访问成员变量的存储空间

思考:最后打印出来的 每个成员变量值是多少?将person.display()换成p->display()成员值会变成多少?

struct Person
{int m_id;int m_age;int m_height;void display(){//eax == &person.m_age == &person+4//mov eax,dword ptr [this]//[eax],[eax + 4],[eax + 8]//[&person + 4],[&person +4 + 4],[&person + 4 + 8]cout<<" id= "<< m_id<<" ,age ="<<m_age<<",height = "<<m_height<<endl;}
}Person person;
person.m_id=10;
person.m_age=20;
person.m_height=30;Person *p=(Person *) &person.m_age;
//eax == &person.m_age == &person + 4
//mov eax,dword ptr[p]
//mov dword ptr [eax + 0],40
//mov dword ptr [&person + 4 + 0],40
p->m_id=40;
//mov dword ptr [eax + 4],50
//mov dword ptr [&person + 4 + 4],50
p->m_age=50;//将person对象的地址传递给display函数的this
person.display();//会将指针p里面存储的地址传递给display函数的this
//将&person.m_age传递给display函数的this
p->display();

答案:10,40,50;

40,50,xxx

0xcc

函数所在栈空间在使用之前存在垃圾数据,需要 用cc来填充

cc->对应 int3(中断):起到断点的作用

当其它语句不小心跳到函数栈空间时,就会在cc的作用下停止执行。

内存

封装

成员变量私有化,提供公共的getter和setter给外界区访问成员变量

#include <iostream>
using namespace std;struct Person
{
private:int m_age;
public:void setAge(int age){if (age <= 0)   return;m_age = age;}int getAge(){return m_age;}};int main()
{Person person;person.setAge(4);cout << person.getAge() << endl;return 0;
}

内存空间的布局

​ 每个应用都有自己独立的内存空间,其内存空间一般都有以下几大部分

代码段(代码区)

​ 用于存放代码,只读

数据段(全局区)

​ 用于存放全局变量等

栈空间

​ 每调用一个函数就会给它分配一段连续的栈空间,等函数调用 完毕后会自动回收这段栈空间

​ 自动分配和回收

堆空间

​ 需要主动申请和释放

​ 在程序运行过程中,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存。

堆空间的申请和释放

malloc/free

#include <iostream>
using namespace std;void test()
{int* p = (int*)malloc(4);*p = 10;free(p);
}void test2()
{char *p=(char *)malloc(4);p[0]=1;p[1]=2;p[2]=3;p[3]=4;/**p=10;*(p+1)=11;*(p+2)=12;*(p+3)=13;*/free(p);//回收所开辟所有空间
}int main()
{test();return 0;
}

在X86环境中

int *p =(int *)malloc(4);
*p=10;

指针p在栈空间中,其占四个字节,存储的是堆空间所开辟的4个字节空间的首地址。

函数调用完毕后栈空间会自动消失,但是堆空间还在,需要free去释放

new/delete

int* p = new int;
*p = 10;
delete p;

new[]/delete[]

char* p = new char[4];
delete[] p;

注意

​ 申请堆空间成功后,会返回那一段内存空间的地址

​ 申请和释放必须是1对1的关系,不然可能会存在内存泄漏

现在很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在

​ 利:提高开发效率,避免内存使用不当或泄漏

​ 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手

**堆空间的初始化 **

memory set

int size = sizeof(int) * 10;
int* p = (int*)malloc(size);
memset(p, 0, 40);
//从p地址开始,将40个字节中每一个字节设置为0;
int *p0 = new int;      //没有初始化
int *p1 = new int();   //初始化为0
int *p2 = new int(5);  //初始化为5
int *p3 = new int[3];  //数组元素未被初始化
int *p4 = new int[3]();    //3个数组元素都被初始化为0
int *p5 = new int[3]{};    //3个数组元素都被初始化为0
int *p6 = new int[3]{5};//数组的首元素被初始化为5,其他为0

memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法

Person person;
person.m_id=1;
person.m_age=20;
person.m_height=180;
memset(&person,0,sizeof(person));Person persons[]={{1,20,180},{2,25,165},{3,27,170}};
memset(persons,0,sizeofz(persons));

对象的内存

​ 对象的内存可以存在于3种地方

全局区(数据段):全局变量

栈空间:函数里面的局部变量

堆空间:动态申请内存(malloc、new等)

//全局区
Person g_person;int main()
{//栈空间Person person;//堆空间Person *p=new Person;return 0;
}

构造函数(Constructor)

构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作。

#include <iostream>
using namespace std;struct Person
{int m_age;Person(){m_age = 0;cout << "Person()" << endl;}Person(int age){m_age = age;cout << "Person(int age)" <<age<< endl;}};int main()
{Person person;Person person2(20);Person person3(30);Person *p=(Person *)malloc(sizeof(Person));p->m_age=10;Person *p1 = new Person; return 0;
}

特点

​ 1.函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数

​ 2.一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象

注意

​ 栈空间和堆空间的创建对象,都会调用构造函数

通过malloc分配的对象,不会调用构造函数(malloc只会申请堆空间)

默认情况下,编译器会为每一个类生成空的无参的构造函数 错误的

在某些特定的情况下,编译器才会为类生成空的无参的构造函数

构造函数的调用

#include <iostream>
using namespace std;struct Person
{int m_age;int m_money;Person(){m_age = 0;cout << "Person()" << endl;}Person(int age){m_age = age;cout << "Person(int age)" <<age<< endl;}};Person g_person0;                                   //Person()调用了无参
Person g_perosn1();                                 //函数声明
Person g_person2(20);                               //有参int main()
{Person person;                                 //无参Person person1();                               //函数声明Person person2(20);                               //有参Person* p0 = new Person;                       //无参Person* p1 = new Person();                     //无参Person* p2 = new Person(20);                   //有参/*4个无参,3个有参*/return 0;
}

成员变量的初始化

#include <iostream>
using namespace std;struct Person
{int m_age;};
//全局区:成员变量初始化为0
Person g_person;int main()
{Person person;//栈空间:没有初始化成员变量//堆空间:没有初始化成员变量Person* p0 = new Person;//堆空间:成员变量初始化为0(有构造函数时不会初始化,编译器认为构造函数会初始化变量)Person* p1 = new Person();Person *p4 = new Person[3];           //成员变量不会被初始化Person *p5 = new Person[3]();      //3个Person对象的成员变量都初始化为0(没有自定义构造函数)Person *p6 = new Person[3]{};      //3个Person对象的成员变量都初始化为0(没有自定义构造函数)cout << g_person.m_age << endl;//cout << person.m_age << endl;cout << p0->m_age << endl;cout << p1->m_age << endl;return 0;
}

如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化

初始化方法

struct Person
{int m_age;Person(){memset(this,0,sizeof(Person));}
}

析构函数(Destructor)

析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作。

#include <iostream>
using namespace std;struct Person
{int m_age;//新的Person对象诞生的过程Person(){cout << "Person::Person()" << endl;}//一个Person销毁的象征~Person(){cout << "~Person()" << endl;}
};int main()
{{//作用域Person person;}return 0;
}

特点

函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个

注意

通过malloc分配的对象free的时候不会调用析构函数

Person *p = (Person*)malloc(sizeof(Person));
free(p);//此时不会调用析构函数

构造函数、析构函数要声明为public,才能被外界正常使用

内存管理:

#include <iostream>
using namespace std;struct Car
{int m_price;Car(){m_price = 0;cout << "Car::Car()" << endl;}~Car(){cout << "Car::~Car()" << endl;}
};struct Person
{int m_age;Car* m_car;//用来做初始化工作Person(){m_age = 0;m_car = new Car();//在堆空间,不会自动回收cout << "Person::person()" << endl;}//用来做内存清理工作~Person(){delete m_car;//这个位置适合cout << "Person::~person()" << endl;}
};int main()
{//内存泄漏:该释放的内存没有释放{Person person;//delete person.m_car;//释放内存,但这样写太麻烦}return 0;
}

//当栈空间person被回收之后,堆空间中的car不会被回收,所以在person中的析构函数中回收car

对象内部申请的堆空间,由对象内部回收

声明和实现分离

具体参见实验

#include <iostream>
using namespace std;class Person
{
private:int m_age;
public:void setAge(int age);int getAge();Person();~Person();
};void Person::setAge(int age)
{m_age = age;
}int Person::getAge()
{return m_age;
}Person::Person()
{m_age = 0;
}Person::~Person()
{}int main()
{getchar();return 0;
}

命名空间

​ 可以避免命名冲突

#include <iostream>
using namespace  std;namespace MJ {int g_age;//它是个全局变量class Person{int m_money;int m_age;};
}class Person
{int m_height;
};int main()
{//法一MJ::g_age=10;MJ::Person person;cout << sizeof(person) << endl;//法二using namespace MJ;g_age=10;Person person;using MJ::g_age;//只使用某一个getchar();return 0;
}

命名空间不影响布局

思考:

namespace MJ
{int g_age;
}namespace FX
{int g_age;
}using namespace MJ;
using namespace FX;//这句代码能通过编译么?
g_age=20;
//不能,具有二义性

命名空间的嵌套

namespace MJ
{namespace SS{int g_age;}
}int main()
{MJ::SS::g_age = 10;using namespace MJ::SS;g_age = 20;using namespace MJ::SS::g_age;g_age = 30;return 0;
}

有个默认的全局命名空间,我们创建的空间默认嵌套在它里面

void func()
{cout<<"func()"<<endl;
}namespace MJ
{void func(){cout<<"MJ::func()"<<endl;}
};int main()
{using namespace MJ;MJ::func();::func();//使用全局命名空间return 0;
}

命名空间的合并

namespace MJ
{int g_age;
}namespace MJ
{int g_no;
}//等价于namespce MJ
{int g_age;int g_no;
}

实验 声明实现和分离

继承

可以让子类拥有父类所有成员函数。

#include <iostream>
using namespace std;struct Person
{int m_age;void run(){}
};struct Student:Person
{int m_age;int m_score;void run() {}void study() {}
};struct Worker:Person
{int m_age;int m_salary;void run(){}
};
int main()
{return 0;
}

C++中没有基类

继承之后对象的内存布局

struct Person
{int m_age;
};struct Student:Person
{int m_no;
};struct GoodStudent:Student
{int m_money;
}int main()
{Person person;//4个字节Strdent stu;  //8个字节GoodStudent gs;//12个字节 }

在内存中父类的成员变量会排布在前面

成员访问权限:

成员访问权限、继承方式有3种:

​ public:公共的,任何地方都可以访问(struct默认)

​ protected:子类内部、当前类内部可以访问

​ private:私有的,只有当前类内部可以访问(class默认)

#include <iostream>
using namespace std;struct Person
{
public:int m_age;void run(){}
};struct Student :protected Person
{void study(){m_age = 10;}
};struct GoodStudent :public Person
{};int main()
{Person person;person.m_age = 10;
}

子类内部访问父类成员的权限,是以下2项中权限最小的那个

​ 成员本身的访问权限

​ 上一级父类的继承方式

开发中用的最多的继承方式是public,这样能保留父类原来的成员访问权限

访问权限不影响对象的内存布局

初始化列表

#include <iostream>
using namespace std;struct Person
{int m_age;int m_height;/*Person(int age,int height){m_age=age;m_height=height;}*/ //等价于Person(int age,int height):m_age(age),m_height(height)//初始化列表{}
};int main()
{Person person(18,180);getchar();return 0;
}

特点

​ 一种便捷的初始化成员变量的方式

​ 只能用在构造函数中

​ 初始化顺序只跟成员变量的声明顺序有关

初始化列表和默认参数的配合使用

struct Person
{int m_age;int m_height;Person(int age=0,int height=0):m_age(age),m_height(height){}
};int main()
{Person person1;Person person2(17);Person person(18,180);
}

如果函数声明和实现是分离的,初始化列表只能写在函数的实现中

struct Person
{int m_age;int m_height;Person(int age=0,int height=0)
};Person(int age=0,int height=0):m_age(age),m_height(height){}int main()
{Person person1;Person person2(17);Person person(18,180);
}

构造函数的互相调用:

构造函数调用构造函数必须写在初始化列表当中,普通函数则不需要

struct Person
{int m_age;int m_height;Person():Person(10,20){}Person(int age,int height);{m_age = age;m_height = height;}
}

**注意:**下面的写法是错误的,初始化的是一个临时对象

struct Person
{int m_age;int m_height;Person(){Person(0,0);}Person(int age,int height):m_age(age),m_height(height){}
}

父类的构造函数:

如果父类没有构造函数,子类不调用,如果有,子类则必须调用

#include <iostream>
using namespace std;struct Person
{int m_age;Person(){cout<<"Person::person()"<<end;}Person(int age){cout<<"Person::person(int age)"<<endl;}
};struct Student:Person
{int m_no;Student(){cout<<"Student::Student()"<<endl;}
};int main()
{Student student;return 0;
}

子类的构造函数会默认调用父类无参的构造函数

如果子类的构造函数显示的调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数

如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数

class Person
{int m_age;
public:Person(int age):m_age(age){}
};class Student:Person
{int m_no;
public:Student(int age,int no):m_no(no),Person(age){}
}int main()
{Student student(18,34);
}

析构函数的调用

class Person
{int m_age;
public:Person(){cout<<"Person::Person()"<<endl;}~Person(){cout<<"Person::~Person()"<<endl;}
};class Student:Person
{int m_no;
public:Student(){//call Person::Person调用父类构造函数cout<<"Student::Student()"<<endl;}~Student(){cout<<"Student::~student()"<<endl;//call Person::~Person() 调用父类析构函数}
}int main()
{{Student student;}return 0;
}

多态

父类指针、子类指针

​ 父类指针可以指向子类对象、是安全的,开发中经常用到(继承方式必须是public)

​ 子类指针指向父类对象是不安全的

#include <iostream>
using namespace std;struct Person
{int m_age;
};struct Student :Person
{int m_score;
};int main()
{//父类子针指向子类对象,父类指针只能访问父类定义的变量,所以它是安全的Person* p = new Student();//而子类指针指向父类对象是不安全的,子类中的变量在父类中是不包含的return 0;
}

默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态

多态是面向对象非常重要的一个特性

​ 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果

​ 在运行时,可以识别出真正的对象类型,调用对应子类中的函数

多态的要素

​ 子类重写父类的成员函数(override),成员函数必须是虚函数

​ 父类指针指向子类对象

​ 利用父类指针调用重写的成员函数

struct Animal
{void speak(){cout<<"Animal::speak()"<<endl;}void run(){cout<<"Animal::run()"<<endl;}
}struct dog:Animal
{//重写(覆写、覆盖、orrivade):要求返回值,函数名,参数要与父类相同void speak(){cout<<"dog::speak()"<<endl;}void run(){cout<<"Animal::run()"<<endl;}
}struct cat:Animal
{void speak(){cout<<"cat::speak()"<<endl;}void run(){cout<<"cat::run()"<<endl;}
}void liu(Animal *p)
{p->speak();p->run();
}int main()
{liu(new dog());//无法实现多态cat *p=(cat *) new Dog();p->speak();//call cat::speakp->run();//call cat::runreturn 0;
}

虚函数

C++中的多态通过虚函数(virtual function)来实现

虚函数:被virtual修饰的成员函数

只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual)

struct Animal
{virtual void speak(){cout<<"Animal::speak()"<<endl;}virtual void run(){cout<<"Animal::run()"<<endl;}
}struct dog:Animal
{//重写(覆写、覆盖、orrivade):要求返回值,函数名,参数要与父类相同void speak(){cout<<"dog::speak()"<<endl;}void run(){cout<<"Animal::run()"<<endl;}
}struct cat:Animal
{void speak(){cout<<"cat::speak()"<<endl;}void run(){cout<<"cat::run()"<<endl;}
}void liu(Animal *p)
{p->speak();p->run();
}int main()
{Animal *p=new cat();p->speak();p->run();liu(new cat());liu(new dog());return 0;
}

虚表

​ 虚函数的实现原理是虚表,这个虚表里面存储着最终要调用的虚函数地址,这个虚表也叫作虚函数表

class Animal
{
public:int m_age;virtual void speak(){cout<<"Animal::speak()"<<endl;}virtual void run(){cout<<"Animal::run()"<<endl;}
};class Cat:public Animal()
{
public:int m_life;void speak(){cout<<"Cat::speak()"<<endl;}void run(){cout<<"Cat::run()"<<endl;}
};int main()
{Animal *cat = new Cat();cat->m_age=20;cat->speak();cat->run();return 0;
}

所有的Cat对象(不管在全局区、栈、堆)共用一份虚表

如果类中有虚函数,则定义该类对象时以右边new的对象类型为标准,如没有,则以左边对象类型为标准

多态使用的场合

一个父类下面包含了好几个子类,每个子类都重写了 父类中的方法

调用父类的成员函数

stuct Animal
{virtual void speak(){cout<<"Animal::Speak()"<<endl;}virtual void run(){cout<<"Animal::run()"<<endl;}
}struct Cat::Animal
{void speak(){Animal::speak();//调用父类的成员函数}
}

虚析构函数

​ 如果存在父类指针指向子类对象的时候,应该将析构函数声明为虚函数(虚析构函数)

​ delete父类 指针时,才会调用子类的析构函数,保证 析构的完整性

stuct Animal
{virtual void speak(){cout<<"Animal::Speak()"<<endl;}virtual void run(){cout<<"Animal::run()"<<endl;}virtual ~Animal(){cout<<"Animal::~Animal"<<endl;}
}struct Cat::Animal
{virtual void speak(){cout<<"Cat::Speak()"<<endl;}virtual void run(){cout<<"Cat::run()"<<endl;}~Cat(){cout<<"Cat::~Cat()"<<endl;}
}int main()
{Animal *cat0=new Cat();cat0->speak;delete cat0;
}

纯虚函数

​ 纯虚函数:没有函数体且初始化为0 的虚函数,用来定义接口规范(类似于Java中的抽象类接口)

struct Animal
{//动物的这些方法有实现是不合理的,所以将其设为纯虚函数virtual void speak()=0; virtual void run()=0;
}struct dog:Animal
{void speak(){cout<<"dog::speak()"<<endl;}void run(){cout<<"Animal::run()"<<endl;}
}struct cat:Animal
{void speak(){cout<<"cat::speak()"<<endl;}void run(){cout<<"cat::run()"<<endl;}
}void liu(Animal *p)
{p->speak();p->run();
}int main()
{Animal *p=new cat();p->speak();p->run();liu(new cat());liu(new dog());return 0;
}

抽象类(Abstract class)

​ 含有纯虚函数的类,不可以实例化(不可以创建对象)

​ 抽象类可以包含非纯虚函数、成员变量

​ 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类

多继承

​ C++允许一个类 可以有多个父类(不建议使用,会增加程序设计复杂度)

class Student
{
public:int m_score;void study(){cout<<"Student::study()"<<endl;}
};class Worker
{
public:int m_salary;void work(){cout<<"Worker::work()"<<endl;}
};class Undergraduate:public Student,public Worker
{
public:int m_grade;void play(){cout<<"Undergraduate::play()"<<endl;}
};

多继承体系下的构造函数调用

struct Student
{int m_score;Student(int score):m_score(score){}void study(){cout<<"Study::study()-score="<<m_score<<endl;}
};struct Worker
{int m_salary;Worker(int salary):m_salary(salary){}void work(){cout<<"Work::work()-salary="<<m_salary<<endl;}
};struct Undergraduate:Student,Worker
{int m_grade;//  Undergraduate(int score,int salary,int grade)//      :m_grade(grade),m_salary(salary),m_score(score){}Undergraduate(int score,int salary,int grade)//多继承体系下的构造函数调用:m_grade(grade),Student(score),Worker(salary){}void play(){cout<<"Undergraduate::play()"<<m_grade<<m_salary<<m_score<<endl;}
}

虚函数

​ 如果子类继承的多个父类都有虚函数,那么子类就会产生对应的多张虚表

struct Student
{virtual void study(){cout<<"Student::study()"<<endl;}
};struct Worker
{virtual void work(){cout<<"Worker::work()"<<endl;}
};struct Undergraduate:Student,Worker
{void study(){cout<<"Undergraduate::study()"<<endl;}void work(){cout<<"Undergraduate::work()"<<endl;}void play(){cout<<"Undergraduate::play()"<<endl;}
}

同名函数

class Student
{
public:void eat(){cout<<"Student::eat()"<<endl;}
};class Worker
{
public:void eat(){cout<<"Worker::eat()"<<endl;}
};class Undergraduate:public Student,public Worker
{
public:void eat(){cout<<"Undergraduate::eat()"<<endl;}
};int main()
{Undergraduate ug;ug.eat();ug.Student::eat();//调用父类中的eat()}

同名成员变量

class Student
{
public:int m_age;
};class Worker
{
public:int m_age;
};class Undergraduate:public Student,public Worker
{
public:int m_age;
};int main()
{Undergraduate ug;ug.m_age=10;ug.Student::m_age=12;return 0;
}

菱形继承

#include <iostream>
using namespace std;struct Person
{int m_age;
};struct Student:Person
{int m_score;
};struct Worker:Person
{int m_salary;
};struct Undergraduate:Student,Worker
{int m_grade;
};int main()
{Undergraduate ug;cout<<sizeof(ug)<<endl;//结果为20return 0;
}

菱形继承带来的问题:

​ 最底下子类从基类继承的成员变量冗余、重复

​ 最底下子类无法访问基类的成员,有二义性

虚继承

​ 虚继承可以解决菱形继承带来的问题

class Person                         //Person 被称为虚基类
{int m_age=1;
};class Student:virtual public Person
{int m_score=2;
};class Worker:virtual public Person
{int m_salary =3;
};class Undergraduate:public Studetn,public Worker
{int m_grade=4;
};int main()
{Undergraduate ug;cout<<sizeof(ug)<<endl;return 0;
}

多继承的应用

#include <iostream>
using namespace std;class JobBaomu
{
public:virtual void clean()=0;virtual void cook()=0;
};class JobTeacher
{
public:virtual void playFootball()=0;virtual void playBaseball()=0;
};class Student:public JobBaomu,public JobTeacher
{int m_score;
public:void clean(){}void cook(){}
};class Worker
{int m_salary;
};int main()
{/*兼职中心,招聘兼职,岗位如下:1.保姆:扫地、做饭2.老师:踢足球、打棒球应聘的角色1.学生2.上班族*/}

静态成员(static)

静态成员:被static修饰的成员变量、函数

可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态变量名)

静态成员变量

​ 存储在数据段(全局区、类似于全局变量),整个程序运行过程中只有一份内存,不需要创建对象就存在

​ 对比全局变量,它可以设定访问权限(public、protected、private),达到局部共享的目的

​ 必须初始化,必须在类外面初始化,初始化时 不能带static,如果类的声明和实现分离(在实现.cpp中初始化)

静态成员函数

​ 内部不能使用this指针(this指针只能用在非静态成员函数内部)

​ 不能是虚函数(虚函数只能是非静态成员函数)PS:虚函数是用于多态,通过父类指针调用虚函数

​ 内部不能访问非静态成员变量、函数,只能访问静态成员变量、函数。

​ 非静态成员函数内部可以访问静态成员变量、函数。

​ 静态成员函数之间可以相互访问。

​ 构造函数和析构函数不能是静态的。

​ 当声明和实现分离时,实现部分不能带static。

#include <iostream>
using namespace std;class Car
{
private:static int m_price;
public:static void run(){cout<<"run()"<<endl;}
};int Car::m_price=0;//初始化int main()
{Car car1;car1.m_price=100;Car car2;car2.m_price=200;Car::m_price=400;Car*p = new Car();p->m_price=500;//静态成员函数访问方式Car.run();Car->run();Car::run();return 0;
}

应用

#include <iostream>
using namespace std;class Car
{
private:static int ms_count;    //统计创造Car对象的多少
public:Car(){//严格来说,这里需要考虑多线程安全问题ms_count++;}static int getCount()     //可以不创建对象,通过类名直接访问,获得ms_count的值{return ms_count;}~Car(){ms_count--;}
};int Car::ms_count=0;int main()
{Car car;Car *p = new Car();return 0;
}

静态成员经典应用----单例模式

单例模式:设计模式的一种,保证某个类永远只创建一个对象

​ 第一步:构造函数、析构函数私有化

​ 第二步:定义一个私有的static成员变量指向唯一的那个单例对象

​ 第三步:提供一个公共的访问单例对象的接口

#include <iostream>
using namespace std;Class Rocket
{
private:Rocket(){};//创建对象必须调用构造函数,一旦私有化就不能调用构造函数~Rocket(){};static Rocket *ms_rocket;//设置为静态是为了保证只有一个Rocket类
public:static Rocket *sharedRocket(){//要考虑多线程安全if(ms_rocket==NULL){ms_rocket=new Rocket();}return ms_rocket;}static void deleteRocket(){//要考虑多线程安全if(ms_rocket!=NULL){delete ms_rocket;//将堆空间回收,但ms_rocket依然有值,不然有可能产生野指针。ms_rocket=NULL;}}void run(){cout<<"run()"<<endl;}};int main()
{Rocket *p1=Rocket::shareRocket();//通过调用静态函数来创建类Rocket *p2=Rocket::shareRocket();Rocket *p3=Rocket::shareRocket();Rocket *p4=Rocket::shareRocket();Rocket::deleteRocket();cout<<p1<<endl;cout<<p2<<endl;cout<<p3<<endl;cout<<p4<<endl;return 0;
}

new、delete误区

int *p = new int;
*p = 10;
delete p;  //回收堆空间4个字节,里面的内容还会在,不会被清除

回收堆空间内存:这块堆空间内存可以重新被别人使用

const成员

const成员:被const修饰的成员变量、非静态成员函数

const成员变量:

​ 必须初始化(类内部初始化),可以在声明的时候直接初始化赋值。

​ 非static的const成员变量还可以在初始化列表中初始化

const成员函数(非静态):

​ const关键字写在参数列表后面,函数的声明和实现必须带const

​ 内部不能修改非静态成员变量

​ 内部只能调用const成员函数、static成员函数

​ 非const成员函数可以调用const成员函数

​ const成员函数和const成员函数构成重载

​ 非const对象(指针)优先调用非const成员函数

​ const对象(指针)只能调用const成员函数、static成员函数

#include <iostream>
using namespce std;class Car
{
public:const int mc_price=0;//直接赋值Car():m_price(0){} //初始化列表中初始化void run() const{cout<<"run()"<<endl;}
};int main()
{return 0;
}

引用类型成员

​ 引用类型成员变量必须初始化(不考虑static情况)

​ 在声明的时候直接初始化

​ 通过初始化列表初始化

class Car
{int age;int &m_price = age;
public:Car(int &price):m_price(price){}
};

拷贝构造函数(Copy Constructor)

​ 拷贝构造函数是构造函数的一种

​ 当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化

​ 拷贝构造函数的格式是固定的,接收一个const引用作为参数

#include <iostream>
using namespace std;class Car()
{int m_price;int m_length;
public:Car(int price=0,int length=0):m_price(price),m_length(length){cout<<"Car(int price=0,int length=0)"<<endl;}//拷贝构造函数Car(const Car &car):m_price(car.m_price),m_length(car.m_length){cout<<"Car(const Car &car)"<<endl;}void display(){cout<<"price="<<m_price<<",length="<<m_length<<endl;}
};int main()
{Car car1;Car car2(100);Car car3(100,5);//利用已经存在的car3对象创建了一个car4新对象//car4初始化时会调用拷贝构造函数Car car4(car3);/*具体拷贝过程car4.m_price=car3.m_price;car4.m_length=car4.m_length;*/getchar();return 0;
}

​ 如果没有拷贝构造函数,在默认情况下,也会将原先对象中所有的值拷贝到新的对象中

调用父类的拷贝构造函数

class Person
{int m_age;
public:Person(int age):m_age(age){}Person(const Person &person):m_age(person.m_age){}
};class Student:public Person
{int m_score;
public:Student(int age,int score):Person(age),m_score(score){}//调用父类的构造函数Student(const Student &student):Person(student),m_score(student,m_score){}//调用父类的拷贝构造函数
}
int main()
{Car car1(100,5);//调用 构造函数Car car2(car1);//调用 拷贝构造函数Car car3=car2;//等价于第二种写法 调用拷贝构造函数Car car4;//调用 构造函数car4=car3;//这里并不会调用拷贝构造函数,仅仅是简单的赋值操作
}

如果子类没有调用父类的拷贝构造函数会默认调用父类的构造函数,如果子类显式的调用了父类的拷贝构造函数,则不会调用父类的构造函数

深拷贝、浅拷贝

浅拷贝(shallow copy)

​ 指针类型的变量只会拷贝地址值,地址拷贝

​ 编译器默认的提供的拷贝是浅拷贝(shallow copy)

​ 将一个对象中所有成员变量的值拷贝到另一个对象

​ 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间

​ 可能会导致堆空间多次free的问题

#include <iostream>
using namespace std;class Car
{int m_price;char *m_name;
public:Car(int price=0,char *name=NULL):m_price(price),m_name(name){}void dispaly(){cout<<"price is"<<m_price<<",name is"<<m_name<<endl;}
};int main()
{const char *name="bmw";char name2[]={"b","m","w","\0"};//没写new就是在栈空间/*char name2[]={"b","m","w","\0"};// "\0"标志着字符串已经结束,必须要写cout<<strlen(name)<<endl;//求字符串长度,"\0"并不被计算,但存储时会将其一起存储cout<<name2<<endl;*/Car *car=new Car(100,name2);//此时m_name存储的是name2的地址,堆空间m_name指向栈空间name2/*堆空间指向栈空间是非常危险的,因为栈空间会被回收,那么堆空间所指向的位置就没有内容,此时堆空间就成为了空指针*/return 0;
}

深拷贝(deep copy)

​ 将指针指向的内容拷贝到新的存储空间

​ 在浅拷贝的基础上,在堆空间继续申请空间,赋值原先堆空间中的内容,内容拷贝

​ 如果要实现深拷贝(deep copy),就需要自定义拷贝构造函数

​ 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间

#include <iostream>
using namespace std;class Car
{int m_price;char *m_name;void copy(const char *name = NULL){if(name==NULL) return;//申请新的堆空间m_name = new char[strlen(name)+1]{};//大括号会将申请堆空间数据清零//拷贝字符串数据到新的堆空间strcpy(m_name;name);}
public:Car(int price=0,const char *name=NULL):m_price(price){/*if(name==NULL) return;//申请新的堆空间m_name = new char[strlen(name)+1]{};//大括号会将申请堆空间数据清零//拷贝字符串数据到新的堆空间strcpy(m_name;name);*/copy(name);}Car(const Car &car):m_price(car.price){/*if(car.m_name==NULL) return;//申请新的堆空间m_name=new char[strlen(name)+1]{};//拷贝字符串数据到新的堆空间strcpy(m_name,car.m_name);*/copy(car.m_name);}void dispaly(){cout<<"price is"<<m_price<<",name is"<<m_name<<endl;}~Car(){if(m_name == NULL) return;delete[] m_name;m_name=NULL;}
};int main()
{Car car1(100,"bmw");Car car2=car1;//默认为浅拷贝car2.display();/*char name[]={'b','m','w','\0'};Car *car = new Car(100,name);car->display();*/return 0;
}

对象型参数和返回值

使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象

class Car
{int m_price;
public:Car(){}Car(int price):m_price(price){}Car(const Car &car):m_price(car.m_price){}
};void test1(Car car)//当传入值时相当于 Car car = car1,相当于拷贝了一个对象,变为引用或指针即可 Car &car
{}Car test2()
{Car car(20);//此处构造一个对象return car;//相当于拷贝构造
};int main()
{Car car1(10);test1(car1);Car car2=test2();//默认构造一个对象,此种调用了一次拷贝构造(编译器做了优化,原来进行了两次),一次普通构造Car car3(30);//构造一个函数,此种调用了一次拷贝构造,两次普通构造car3 = test2();//并没有创建一个新对象,只是简单的复制
}

匿名对象(临时对象)

​ 没有变量名、没有被指针指向的对象,用完后马上调用析构

class Car
{};int main()
{Car();//匿名对象,创建结束后就会被回收             return 0;
}

隐式构造、explicit

隐式构造(转换构造)

C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数

可以通过explicit 禁止隐式构造

class Person
{int m_age;
public:Person(){cout<<"Person()-"<<this<<endl;}explicit Person(int age):m_age(age){cout<<"Person(int)-"<<this<<endl;}Person(const Person &person){cout<<"Person(const Person &person)-"<<this<<endl;}~Person(){cout<<"~Person()-"<<this<<endl;}void display(){cout<<"display()-age is"<<this->m_age<<endl;}
};void test1(Person person=30)
{}Person test2()
{return 40;
}int main()
{/*Person p1;Person p2(10);Person p3=p2;*/Person p1=20;//调用单参数构造函数,Person(int age),等价于Person p1(20)test1(30);test2();Person p1;//先构造一个对象p1=40;//隐式构造一个对象return 0;
}

编译器自动生成的构造函数

​ C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如

​ 1.成员变量在声明的同时进行了初始化

​ 2.有定义虚函数

​ 3.虚继承其它类

​ 4.包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)

​ 5.父类有构造函数(编译器生成或自定义)

总结:

​ 对象创建时 ,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数

/*
很多教程都说:编译器会为每一个类都生成空的无参的构造函数       错误的
*/class Person
{
public:    int m_price=5;//成员变量在声明的同时进行了初始化//等价于/*int m_price;Person(int m_price=5){}*/virtual void run(){}}int main()
{Person person;return 0;
}

友元

​ 友元包括友元函数和友元类

​ ???如果将函数A(非成员函数)声明为类C的友元函数,那么在函数A内部就能直接访问类C对象的所有成员

​ 如果将类A声明为类C的友元类,那么在类A的所有成员函数内部都能直接访问类C对象的所有成员

class Point
{friend Point add(Point,Point);//友元函数friend class Math;//友元类int m_x;int m_y;
public:int getX(){return m_x};int getY(){return m_y};Point(int x,int y):m_x(x),m_y(y){}void display(){cout<<"("<<m_x<<","<<m_y<<")"<<endl;}
};Point add(Point p1,Point p2)
{//return Point(p1.getX()+p2.getX(),p1.getY()+p2.getY());//调用过于频繁,浪费空间return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);//定义为友元函数之后就可以被允许直接访问私有成员变量
}class Math
{
public:void display(Point p1,Point p2){cout<<p1.m_x<<","<<p2.m_y<<endl;}
};
int main()
{Point p1(10,20);Point p2(20,30);return 0;
}

内部类

如果将类A定义在类C的内部,那么类A就是一个内部类(嵌套类)

内部类的特点:

​ 支持public、protected、private权限

​ 成员函数可以直接访问其外部类对象的所有 成员(反过来则不行)

​ 成员函数可以直接不带类名、对象名访问其外部类的static成员

​ 不会影响外部类的内存布局

​ 可以在外部类内部声明,在外部类外面进行定义

#include <iostream>
using namespace std;class Person
{static int ms_price;int m_age;
public:         //protected只有类内部以及子类能够使用,private只有类内部能够使用class Car{int m_price;ms_price=0;void run(){cout<<m_age<<endl;}};
};int main()
{Person::Car car1;//定义Car对象,return 0;
}

内部类-声明和实现分离

class Point
{class Math{void test();};
};void Poinnt::Math::test()
{}
class Point
{class Math;
};class Point::Math
{voit test(){}
};
class Point
{class Math;
};class Point::Math
{voit test();
};voit Point::Math::test()
{}

局部类

在一个函数内部定义的类,称为局部类

局部类的特点

​ 作用域仅限于所在的函数内部

​ 其所有成员必须定义在类内部,不允许定义static成员变量

​ 成员函数不能直接访问函数的局部变量(static变量除外)

局部变量加上static修饰,其作用范围相当于全局变量

void test()
{static int age=10;//局部类  class Car{void run(){}};
}int main()
{return 0;
}

运算符重载(operator overload)

运算符重载(操作符重载):可以为运算符增加一些新的功能

全局函数、成员函数都支持运算符重载

class Point
{friend Point add(Point,Point);friend void operator+(Point,Point);friend ostream &operator<<(ostream &,const const Point &);friend istream &operator>>(istream &,Point &);int m_x;int m_y;
public:Point(int x,int y):m_x(x),m_y(y){}void display(){cout<<"("<<m_x<<","<<m_y<<")"<<endl;}Point(const Point &point){m_x=point.m_x;m_y=point.m_y;}const void operator+(const Point &point) const//第一个const是用来限制返回值是个常量对象,不能给它赋值,第二个const是用来限制此函数为const函数,保证返回值能够再次调用此函数{return Point(this->m_x+point.m_x,this->m_y+point.m_y);}Point &operator+=(const Point &point){m_x+=point.m_x;m_y+=point.m_y;return *this;}bool operator==(const Point &point){return (m_x==point.m_x)&&(m_y==point.m_y);}const Point operator-() const{return Point(-m_x,-m_y);}Point &operator++()//前置{m_x++;m_y++;return *this;}Point operator++(int)//后置{Point old(m_x,m_y);m_x++;m_y++;return old;}};/*Point add(Point p1,Point p2)
{return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);
}*//*void operator+(const Point &p1,const Point &p2)
{return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);
}*///output stream -> ostream
ostream &operator<<(ostream &cout,const Point &point)
{cout<<"("<<point.m_x<<","<<point.m_y<<")";return cout;
}//input stream -> istream
istream &operator>>(istream &cin,Point &point)
{cin>>point.m_x;cin>>point.m_y;return cin;
}int main()
{Point p1(10,30);Point p2(20,30);Point p3(30,30);//Point p3=add(p1,p2);//Point p3=p1+p2+p3;p3.display();p1+p2;//当重载写到类中时,相当于p1.operator+(p2);p1+=p2;(p1+=p2)=Point(40,50);-p1;Point p3=-(-p1);cout<<p1;//等价于operator(cout,p1)cin>>p1;cin>>p1>>p2;return 0;
}

调用父类的运算符重载函数

#include <iostream>
using namespace std;class Person
{
public:int m_age;   Person &operator=(const Person &person){m_age=person.m_age;}
};class Student:public Person
{
public:int m_score;Student &operator=(const Student &student){Person::operator=(student);//调用父类的运算符重载函数m_score=student.m_score;}
}int main()
{Student stu1;stu1.m_age=20;stu1.m_score=100;return 0;
}

仿函数

将一个对象当作一个函数一样来使用

对比普通函数,它作为对象可以保存状态;

#include <iostream>
using namespace std;class Sum
{int m_age;
public:int operator()(int a,int b){return a+b;}void func(){}};int main()
{Sum sum;cout<<sum(10,20)<<endl;return 0;
}

运算符重载注意点

有些运算符重载不可以被重载,比如

​ 对象成员访问运算符: .

​ 域运算符:::

​ 三目运算符:?:

sizeof

有些运算符只能重载为成员函数,比如

​ 赋值运算符:=

​ 下标运算符:[]

​ 函数运算符:()

​ 指针访问运算符:->

模板(template)

泛型,是一种将类型参数化以达到代码复用的技术,C++中使用模板来实现泛型

模板的使用格式

​ template <typename\class T>

​ typename和class是等价的

模板没有被使用时,是不会被实例化出来的

#include <iostream>
using namespace std;class Point
{friend ostream& operator<<(ostream& , const Point&);int m_x;int m_y;
public:Point(int x, int y) :m_x(x), m_y(y) {}Point operator+(const Point& point){return Point(m_x + point.m_x, m_y + point.m_y);}
};ostream& operator<<(ostream& cout, const Point& point)
{return cout << "(" << point.m_x << "," << point.m_y << ")";
}//int add(int a, int b)
//{
//  return a + b;
//}
//
//double add(double a, double b)
//{
//  return a + b;
//}
//
//Point add(Point a, Point b)
//{
//  return a + b;
//}template <typename T> T add(T a, T b)//泛型
{return a + b;
}int main()
{add<int>(10, 20);/*cout << add(10, 20) << endl;cout << add(1.5, 1.6) << endl;*/return 0;
}

模板的声明和实现如果分离到.h和.cpp中,会导致链接错误

一般将模板的声明和实现统一放到一个.hpp文件中

参数模板

template<class T>
void swapValues(T &v1,T &v2)
{T temp = v1;v1 = v2;v2 = temp;
}int main()
{int a=10;int b=20;swapValues<int>(a,b);swapValues(a,b);
}

多参数模板

template<class T1,class T2>
void display(const T1 &v1,const T2 &v2)
{cout<<v1<<endl;cout<<v2<<endl;
}

动态数组、类模板

具体内容请见 实验 类模板

#include <iostream>
using  namespace std;class Point
{int m_x;int m_y;
public:Point(int x, int y) :m_x(x), m_y(y) {}
};template<typename Element>
class Array
{//用于指向首元素Element* m_data;//元素个数int m_size;//容量int m_capacity;public:Array(int capacity=0){m_capacity = (capacity > 0) ? capacity : 10;//申请堆空间m_data = new Element[m_capacity];}~Array(){if (m_data == NULL) return;delete[] m_data;}void add(Element value){if (m_size == m_capacity){//扩容/** 1.申请一块更大的存储空间* 2.将就空间的数据拷贝到新空间* 3.释放旧空间*/cout << "空间不够" << endl;return;}m_data[m_size++] = value;}Element get(int index){if (index < 0 || index >= m_size){//报错,抛出异常throw "数组下标越界";}return m_data[index];}int size(){return m_size;}Element operator[](int index){return get(index);}
};int main()
{Array<int> array(3);array.add(10);array.add(20);array.add(30);array.add(40);array.add(50);cout << array.get(0) << endl;cout << array[1] << endl;cout << array.size() << endl;Array<Point> array1(2);Point point(1, 2);array1.add(Point(1, 2));array1.add(Point(3, 4));return 0;
}

类型转换

c语言风格的类型转换符

​ (type)expression

​ type(expression)

int a=10;
double d=(double) a;
double d2=double(a);

C++中有4个类型转换符

​ static_cast

​ dynamic_cast

​ reinterpret_cast

​ const_cast

使用格式:XX_cast(expression)

int a = 10;
double d = static_cast<double>(a);

const_cast

一般用于去除const属性,将const转换成非const

const Person *p1 = new Person();
p1->m_age = 10;Person *p2 = const_cast<Person *>(p1); //将const转换为非const,p1和p2的值相同
Person *p3=(Person *)p1;   //与上面没区别,这是c语言的写法
p2->m_age = 20;

dynamic_cast

一般用于多态类型的转换,有运行时安全检测,不安全时将指针赋值为空指针

class Person
{virtual void run(){}
};class Student:public Person{};class Car{};int main()
{Person *p1 = new Person();Person *p2 = new Student();Student *stu1 = (Student *)p1;//不安全,子类指针指向父类,不会进行安全检测Student *stu2 = dynamic_cast<Studnet *>(p2);//安全Car *c1 = (Car*)p1;//没有检测Car *c2 = dynamic_cast<Car *>(p2);//不安全,没有赋值return 0;
}

static_cast

对比dynamic_cast,缺乏运行时安全检测

不能交叉转换(不是同一继承体系的,无法转换)

​ 交叉转换:没有任何联系的两个类之间进行的转换

常用于基本数据类型的转换、非const转成const

reinterpret_cast

属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝

Person *p1 = new Person();
Person *p2 = new Student();
Student *stu1 = reinterpret_cast<Student *>(p1);
Student *stu2 = reinterpret_cast<Student *>(p2);
Car *car = reinterpret_cast<Car *>(p1);int *p = reinterpret_cast<int *>(100);
int num = reinterpret_cast<int>(p);int i = 10;
double d1 = reinterpret_cast<double &>(i);//结果并不相等

C++11新特性

auto

可以从初始化表达式中推断出变量的类型,大大简化编程工作

属于编译器特性,不影响最终的机器码质量,不影响运行效率

auto i = 10;//int
auto p = new Person 

decltype

可以获取变量类型

int a = 10;
decltype(a) b = 20;//int

nullptr

可以解决NULL二义性的问题

int *p1 = nullptr;//空指针

快速遍历

int array[]={1,2,3,4};
int array[]{1,2,3,4};//更简洁的初始化数组方式
for(int item:array)
{cout<<item<<endl;
}

Lambda表达式

有点类似于JavaScript中的闭包、iOS中的Block,本质就是函数

完整结构:

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

capture list:捕获外部变量列表

params list:形参列表,不能使用默认 参数,不能省略参数名

mutable:用来使用是否可以修改捕获的变量

exception:异常设定

return type:返回值类型

function body:函数体

有时可以省略部分结构

[capture list](params list)->return type{function body}
[capture list](params list){function body}
[capture list]{function body}
int add(int v1,int v2)
{return v1 + v2;
}int sub(int v1,int v2)
{return v1 - v2;
}int multiple(int v1,int v2)
{return v1 * v2;
}int divide(int v1,int v2)
{return v1 / v2;
}int exec(int v1,int v2,int (*func)(int ,int ))
{return func(v1,v2);
}int main()
{///cout<<exec(10,20,add)<<endl;cout<<exec(10,20,sub)<<endl;cout<<exec(10,20,multiple)<<endl;cout<<exec(10,20,divide)<<endl;//等价于cout<<exec(20,10,[](int v1,int v2){return v1+v2;})<<endl;cout<<exec(20,10,[](int v1,int v2){return v1-v2;})<<endl;cout<<exec(20,10,[](int v1,int v2){return v1*v2;})<<endl;cout<<exec(20,10,[](int v1,int v2){return v1/v2;})<<endl;   ///([]                 //最简单的lambda表达式{cout<<"func"<<endl;})();          //调用lambda表达式///void (*P)()=[]{cout<<"func"<<endl;}              //存储lambda表达式auto p=[]{cout<<"func"<<endl;//等价于上面}p();p();///auto p = [](int a,int b)->int//带有返回值的lambda表达式{return a+b;}cout<<p(10,20)<<endl;///return 0;
}

变量捕获

int main()
{int a=10;//默认都是值捕获auto func = [a]          //变量捕获{cout<<a<<endl;};//地址捕获auto func = [&a]          //变量捕获{cout<<a<<endl;};//隐式捕获(值捕获)auto func = [=]{cout<<a<<endl;};//隐式捕获(地址捕获)auto func = [&]{cout<<a<<endl;}a=20;func();return 0;
}

mutable

int a=10;
/*auto func = [&a]
{a++;
};*/
//等价于上面
auto func = [a]()mutable
{a++;//11
};func()
cout<<a<<endl;//10
return 0;

C++14

泛型Lambda表达式

auto func = [](auto v1,auto v2){return v1+v1;};
cout<<func(10,20.5)<<endl;

对捕获的变量进行初始化

int a;
auto func = [a = 10]()
{cout<<a<<endl;
};
func();cout<<a<<endl;

C++17

可以进行初始化的if、switch语句

//变量a,b的作用域使它所在的if语句、以及其后面的if-else语句
if(int a=10;a>10)
{a=1;
}
else if(int b=20;a>5&&b>10)
{b=2;a=2;
}
else if(0)
{b=3;a=3;
}
else
{b=4;a=4;
}//变量a的作用域是它所在的switch语句
switch(int a=10,a)
{case 1:break;case 2:break;default:break;
}

异常

编程过程中的常见错误类型

​ 语法错误

​ 逻辑错误

​ 异常

异常时一种在程序运行过程中可能会发生的错误(比如内存不够)

异常没有被处理,会导致程序终止

int main()
{cout<<1<<endl;for(int i=0;i<9999;i++){//这句代码可能会产生异常(抛出异常)、系统抛出异常try{int *p=new int[999999];}catch(){cout<<"产生异常:内存不够用";break;}}
}

抛出异常

throw异常后,会在当前函数中查找匹配的catch,找不到就终止当前函数代码,去上一层函数中查找。如果最终都找不到匹配的catch,整个程序就会终止。

int divide(int v1,int v2)
{if(v2==0){throw "不能除以0"}
}int main()c
{int a=10;int b=0;try{cout<<divide(a,b)<<endl;}catch(const char * exception){cout<<"产生异常"<<exception<<endl;}catch(int exception){cout<<"产生异常"<<exception<<endl;}
}

异常的抛出声明

为了增强可读性和方便团队协作,如果函数内部可能会抛出异常,建议函数声明一下异常类型

//抛出任意可能的异常
void func1()
{}//不抛出任何异常
void func2() throw()
{}//只抛出int、double类型的异常
void func3() throw(int,double)
{}

自定义异常类型

//所有异常的基类
class Exception
{
public:virtual const char *what() const=0;
};class DivideException:public Exception
{
public:const char *what() const{return "不能除以0";}
};int divide(int v1,int v2)
{if(v2==0){//抛出异常throw DivideException();}return v1/v2;
}void test()
{try{int a = 10;int b = 0;cout<<divide(a,b)<<endl;}catch(const DivideException &exception){cout<<"产生了异常(DivideException)"<<exceptioin.what()<<endl;}
}

拦截所有类型的异常

try
{int a=10;int b=0;int c = dibide(a,b);
}
catch(...)//拦截所有类型的异常
{cout<<"出现异常"<<endl;
}

标准异常(std)

系统自带的异常

智能指针(Smart Pointer)

传统指针存在的问题

​ 需要手动管理内存

​ 容易发生内存泄漏(忘记释放、出现异常等)

​ 释放之后产生野指针

智能指针就是为了解决传统指针存在的问题

​ auto_ptr:属于C++98标准,在C++11中已经不推荐使用(有缺陷,不如不能用于数组)

​ shared_ptr:属于C++11标准

​ unique_ptr:属于C++11标准

#include <iostream>
using namespace std;class Person
{
public:int m_age;Person(){cout << "Person()" << endl;}Person(int age):m_age(age){}~Person(){cout << "~Person()" << endl;}void run(){cout << "run()-" << m_age << endl;}
};void test()
{//Person* p = new Person(20);//可以理解为:智能指针p指向了堆空间的Person对象auto_ptr<Person> p(new Person(20));p->run();
}int main()
{test();{//会报错,auto_ptr不能指向数组auto_ptr<Person> p(new Person[10]);p->run()}{shared_ptr<Person[]> p(new Person[30]);//share_ptr 这样使用cout<<p1.use_count()<<endl;//查看强引用计数shared_ptr<Person[]> p2=p;shared_ptr<Person[]> p3=p2;p->run();              }return 0;
}

智能指针的简单自实现

template<class T>
class SmartPointer
{T *m_pointer;
public:SmartPointer(T* pointer):m_pointer(pointer){}~SmartPointer(){if(m_pointer == nullptr) return;delete m_pointer;}T *operator->(){return m_pointer;}
};

​ 智能指针就是对传统指针的再度封装

shared_ptr

shared_ptr的设计理念

​ 多个shared_ptr可以指向同一个对象,当最后一个shared_ptr在作用域范围内结束时,对象才会释放

可以通过一个已存在的智能指针初始化一个新的智能指针

shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2(p1);

针对数组的用法

shared_ptr<Person> ptr1(new Person[5]{},[](Person  *p){delete[] p;})

原理

一个shared_ptr会对一个对象产生强引用(strong reference)

每个对象都有个与之对应的强引用计数,记录着当前对象被多少个shared_ptr强引用着

当有一个新的shared_ptr指向对象时,对象的强引用计数就会+1

当有一个shared_ptr销毁时(比如作用域结束),强引用计数就会-1

当一个对象的强引用计数为0时(没有任何shared_ptr指向对象时),对象就会自动销毁(析构函数)

shared_ptr的循环引用

只针对智能指针

​ weak_ptr 会对一个对象产生弱引用

​ weak_ptr 可以指向对象解决shared_ptr的循环引用问题

class Person;class Car
{
public:shared_ptr<Person> m_person = nullptr;//将其变为weak_ptr<Person> m_person = nullptr,不会使强引用加1Car(){cout<<"Car()"<<endl;}~Car(){cout<<"~Car()"<<endl;}
};class Person
{
public:shared_ptr<Car> m_car = nullptr;Person(){cout<<"Person()"<<endl;}~Person(){cout<<"~Person()"<<endl;}
};int main()
{{//会导致内存泄漏shared_ptr<Person> Person(new Person());shared_ptr<Car> car(new Car());person->m_car = car;car->m_person = person;}return 0;
}

unique_ptr

unique_ptr也会对一个对象产生强引用,它可以确保统一时间只有一个指针指向对象

当unique_ptr销毁时(作用域结束时),其指向的对象也就自动销毁了

可以使用std::move函数转移unique_ptr的所有权

class Person;class Car
{
public:shared_ptr<Person> m_person = nullptr;Car(){cout<<"Car()"<<endl;}~Car(){cout<<"~Car()"<<endl;}
};class Person
{
public:shared_ptr<Car> m_car = nullptr;Person(){cout<<"Person()"<<endl;}~Person(){cout<<"~Person()"<<endl;}
};int main()
{//ptr1强引用着Person对象unique_ptr<Person> ptr1(new Person());//转移之后,ptr2强引用着person对象unique_ptr<Person> ptr2 = std::move(ptr1);return 0;
}

C++中的重难点看这一篇就够了相关推荐

  1. 关于Java中的锁,看这一篇就够了(总结篇)

    文章目录 锁的概念 锁的分类 一.什么是悲观锁? Java中的悲观锁有哪些 synchronized 基于AQS的实现类 二.什么是乐观锁? Java中的乐观锁有哪些 Valotile Java内存模 ...

  2. 代码中应用设计模式,看这一篇就够了

    作者:宁愿呢 cnblogs.com/liyus/p/10508681.html 为什么要使用设计模式 因为我们的项目的需求是永远在变的,为了应对这种变化,使得我们的代码能够轻易的实现解耦和拓展.如果 ...

  3. Idea 中的 Git 操作看这一篇就够了(最全的讲解,文章比较长,截图比较多是为了说明问题)

    环境部分略过 从0开始创建一个项目,用来讲解git,会包括创建远程新分支,回滚等操作. 在每一个讲解过程中都会有问题提问 基础部分讲解 实战高级部分 基础部分内容 如果在github上已有仓库    ...

  4. 面试中的工具问题 看这一篇就够了

    根据很多面试人员的经历,我们总结出了在面试中大概率会出现以下问题. Monkey怎么用的? ①. 可以通过monkey命令来进行使用,基本命令是adb shell monkey -p 包名 -s 序列 ...

  5. Qt中QMessageBox的用法---看这一篇就够了

    一.详细说明 QMessageBox类提供一个模态对话框,用于通知用户或询问用户一个问题并接收答案. 消息框显示一个主要文本,用于向用户提示某个情况;显示一个信息文本,用于进一步解释警报或向用户询问问 ...

  6. api网关选型_如何轻松打造百亿流量API网关?看这一篇就够了(下)

    如何轻松打造百亿流量API网关?看这一篇就够了(上) 上篇整体描述了网关的背景,涉及职能.分类.定位环节,本篇进入本文的重点,将会具体谈下百亿级流量API网关的演进过程. 准备好瓜子花生小板凳开始积累 ...

  7. 基础 | 零散的MySql基础记不住,看这一篇就够啦

    ❝ 这是小小本周的第二篇,本篇将会着重的讲解关于MySql基础的内容,MySql基础看这一篇就够啦. ❞ 送书反馈与继续送书 之情小微信公众号第一次送书,Java深度调试技术,书已经被中奖者麦洛签收, ...

  8. 1盒子刷Armbian,看这一篇就够了——Armbian全方位安装指导书

    N1盒子刷Armbian,看这一篇就够了--Armbian全方位安装指导书 2020-03-30 19:04:11 72点赞 715收藏 122评论 创作立场声明:一个写完之后很累很累的年轻人 追加修 ...

  9. 如何应对大数据分析工程师面试Spark考察,看这一篇就够了

    作者丨斌迪.HappyMint 来源丨大数据与人工智能(ID:ai-big-data) [导读]本篇文章为大家带来spark面试指南,文内会有两种题型,问答题和代码题,题目大部分来自于网络上,有小部分 ...

最新文章

  1. 主角用计算机控制身体,国漫段子手:绝技,用计算机控制挖掘机炒菜
  2. Windows下dump文件生成与分析
  3. 深度学习(十九)——FCN, SegNet, DeconvNet, DeepLab, ENet, GCN
  4. 利用永恒之蓝入侵服务器复制文件,msf利用永恒之蓝进行漏洞攻击
  5. HTTP Error 415: Unsupported Media Type! 这个错误
  6. ffdshow 源代码分析 6: 对解码器的dll的封装(libavcodec)
  7. 基于centos的FasfDFS安装配置
  8. Python 路径处理(os.path模块)
  9. mvc npoi将List实体导出excel的最简单方法
  10. 使用Eclipse设置java源代码 连接
  11. 破解网址_中国目前的破解组织大全
  12. 谷歌gmail注册入口_如何更改,恢复,重设Gmail,Google云端硬盘,Android,Chrome的Google帐户密码?...
  13. 浙江大学计算机学院陈越教授,科学网—浙大教授陈越:快乐教学 行者无疆
  14. 【微信小程序】echarts 正态分布图 区间柱形图
  15. ios android 联机游戏平台,iOS 和安卓游戏终于互通了
  16. 【谷歌浏览器】谷歌浏览器SameSite
  17. .NET周报【11月第4期 2022-11-30】
  18. Android P2P 通信方案探索
  19. VSCode、notepa++列选择(垂直选中)快捷键方法
  20. TrackMouseEvent处理鼠标消息

热门文章

  1. 定制iOS 7中的导航栏和状态栏
  2. 【题解】大床Nim (2019,5.23)
  3. 关于测试用例的一些思考
  4. Python3.2官方文档翻译--作用域和命名空间实例
  5. 中国金融出版社出版的2016版《个人贷款》
  6. 在Linux中发现IP地址冲突的方法
  7. [JAR包] android引入JAR包,打包成JAR包,打包成Library项目,导入Library项目
  8. 21天让你成为Horizon View高手—Day20:证书管理
  9. 开发工程师的职场人生路(转)
  10. 十三个代码注释的小技巧