C++
纯虚函数
它的申明格式如下:
class CShape
{
public:
virtual void Show()=0;
};
在普通的虚函数后面加上"=0"
这样就声明了一个pure virtual function
.
在什么情况下使用纯虚函数?
- 当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
- 这个方法必须在派生类(derived class)中被实现;
手写实现shared_ptr
#include <string>
#include <iostream>
using namespace std;
template <typename T>
class Shared_ptr {
public:
// 空参构造 空指针
Shared_ptr():count(0), _ptr((T*)0) {}
// 构造函数 count必须new出来
Shared_ptr(T* p) : count(new int(1)), _ptr(p) {}
// 拷贝构造函数 使其引用次数加一
Shared_ptr(Shared_ptr<T>& other) : count(&(++ *other.count)), _ptr(other._ptr){}
// 重载 operator*和operator-> 实现指针功能
T* operator->() { return _ptr; }
T& operator*() { return *_ptr; }
// 重载operator=
// 如果原来的Shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
// 然后将新的对象引用次数加一。
Shared_ptr<T>& operator=(Shared_ptr<T>& other)
{
if (this == &other)
return *this;
++*other.count;
if (this->_ptr && 0 == --*this->count)
{
delete count;
delete _ptr;
cout << "delete ptr =" << endl;
}
this->_ptr = other._ptr;
this->count = other.count;
return *this;
}
// 析构函数 使引用次数减一并判断引用是否为零(是否调用delete)。
~Shared_ptr()
{
if (_ptr && --*count == 0)
{
delete count;
delete _ptr;
cout << "delete ptr ~" << endl;
}
}
int getRef() { return *count; }
private:
int* count; // should be int*, rather than int
T* _ptr;
};
虚析构函数
虚析构函数是为了解决这样的一个问题:***基类的指针指向派生类对象,并用基类的指针删除派生类对象。***如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。
防止头文件循环声明
在c语言中,对同一个变量或者函数进行多次声明是不会报错的。所以如果h文件里只是进行了声明工作,即使不使用#ifndef
宏定义,多个c文件包含同一个h文件也不会报错。
但是在c++语言中,#ifdef
的作用域只是在单个文件中。所以如果h文件里定义了全局变量,即使采用#ifdef
宏定义,多个c文件包含同一个h文件还是会出现全局变量重定义的错误。
使用#ifndef
可以避免下面这种错误:如果在h文件中定义了全局变量,一个c文件包含同一个h文件多次,如果不加#ifndef
宏定义,会出现变量重复定义的错误;如果加了#ifndef
,则不会出现这种错误。
另一种实在头文件中使用#pragma once
语句
C++ #include " " 与 <>有什么区别?
简言之 #include <>
和 #include ""
都会在实现定义的位置查找文件,并将其包含。
区别是若 #include ""
查找成功,则遮蔽 #include <>
所能找到的同名文件;否则再按照#include <>
的方式查找文件。另外标准库头文件都放在 #include <>
所查找的位置。一般来说 #include <>
的查找位置是标准库头文件所在目录, #include ""
的查找位置是当前源文件所在目录。不过这些都可由编译器调用参数等配置更改。
socket编程
服务端编程:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:绑定套接字到一个IP地址和一个端口上(bind());
3:将套接字设置为监听模式等待连接请求(listen());
4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
5:用返回的套接字和客户端进行通信(send()/recv());
6:返回,等待另一连接请求;
7:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
客户端编程:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:向服务器发出连接请求(connect());
3:和服务器端进行通信(send()/recv());
4:关闭套接字,关闭加载的套接字(closesocket()/WSACleanup())。
单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
- 避免对资源的多重占用(比如写文件操作)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
- 饿汉模式:即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。由静态初始化实例保证其线程安全性,WHY?因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
- 懒汉模式:即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。需要用锁,来保证其线程安全性:原因:多个线程可能进入判断是否已经存在实例的if语句,从而non thread safety。使用double-check来保证thread safety。但是如果处理大量数据时,该锁才成为严重的性能瓶颈。
实现:
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。
单例的实现主要是通过以下两个步骤:
将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用 (故声明为private)
//饿汉式实现一:直接定义静态对象
class Singleton
{
public:
static Singleton& GetInstance();
private:
Singleton(){}
Singleton(const Singleton&);
Singleton& operator= (const Singleton&);
private:
static Singleton m_Instance;
};
//CPP文件
Singleton Singleton::m_Instance;//类外定义-不要忘记写
Singleton& Singleton::GetInstance()
{
return m_Instance;
}
//函数调用
Singleton& instance = Singleton::GetInstance();
//饿汉式实现方式二:静态指针 + 类外初始化时new空间实现
class Singleton
{
protected:
Singleton(){}
private:
static Singleton* p;
public:
static Singleton* initance();
};
Singleton* Singleton::p = new Singleton;
Singleton* singleton::initance()
{
return p;
}
懒汉式:
饿汉式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YskRUM5i-1571478718918)( https://uploadfiles.nowcoder.com/images/20190313/311436_1552475817756_0ABB4D7B05F0CEEF9D60F6FD5B1F4040 )]
结构体大小
-
每个成员的偏移量都必须是当前成员所占内存大小的整数倍。如果不是,编译器会在成员之间加上填充字节。
-
当所有成员大小计算完毕后,编译器判断当前结构体大小是否是结构体中最宽的成员变量大小的整数倍。如果不是,会在最后一个成员后做字节填充。
struct node1
{
int a;
int b;
char c;
char d;
};
struct node2
{
int a;
char b;
int c;
char d;
};
node1大小为12字节, node2大小为16字节。 **结构体里面的动态数组int a[]
,即不指定大小的数组,sizeof
时不包括该数组的大小 **。
动态库和静态库
静态库:(.a , .lib)
之所以成为静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件
动态库:(.so , .dll)
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。**不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。**动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
手撕strcpy
功能:把 src 所指向的字符串复制到 dest。
看到这个需求你一想,这还不简单,就写出了下面代码,可惜啊,你挂了!
char *strcpy(char *dest, const char *src) {
if (!dest || !src)
return NULL;
char *d = dest;
while (*src != '\0') {
*d++ = *src++;
}
*d = '\0';
return dest;
}
考虑一个场景,现在测试程序调用如下:
char src[66] = "lightcity";
strcpy(src + 1, src, strlen(src) + 1);
你跑一下你的代码发现运行结果是:lllllllll。
原因:内存重叠!
在上述代码中:src+1指向的是i这个位置src指向的是l这个位置
当指针dst赋值为l的时候,前面的i已经被改为l,依次类推,就输出了lllllllll。
我们分析一下内存重叠的位置:
内存重叠:当 src<dst<src+n n为字符串的长度,不包含\0。
实现如下:
char *strcpy1(char *dest, const char *src) {
if (!dest || !src)
return NULL;
char *d = dest;
int size = strlen(src) + 1;
if (d > src && d < src + size) {
d = d + size - 1;
src = src + size - 1;
while (size--) {
*d-- = *src--;
}
} else {
while (size--) {
*d++ = *src++;
}
}
}