C++/Java知识点

基础知识点

指针与变量

无论是int *p,还是void *p,不会对存储的值有影响

但是嘞,如果要存储整形数字的话,是需要声明类型的,比如

1
2
3
4
5
6
void *p=&a;
*p=8; //这就会报错

所以是
int *p=&a;
*p=8;

这就是指针的工作原理

看下,下面这个,分配内存

1
2
3
4
5
6
7
8
9
10
#include<string.h>
...
int main(){

char *buffer=new char[8];
memset(buffer,1,8);//将内存的内容设置为指定值

//delete[] buffer;
}

分配8个字节内存

开辟了8个字节,存储了一个指向该数据开头的指针

双指针嘞,那值就变成地址了

1
2
3
4
char *buffer=new char[8];
memset(buffer,1,8);

char** ptr=&buffer;

双指针

在十六进制表示中0x1c7080侧是最高有效字节(MSB),侧是最低有效字节(LSB)。

大端:高地址存低字节,(人类的顺序)

小端:低地址存低字节

堆中的变量(程序员自己new或malloc的):分配的内存一般是按照地址递增的顺序存储的。堆往高地址生长,先声明的变量位于低地址

栈中的变量:栈往低地址生长,先声明的变量位于高地址

1.虚函数

virtual 类型 成员函数名 (参数表);

虚函数必须存在于类的继承环境中才有意义

存在虚函数的类都有一个一维的虚函数表叫虚表,类的对象有一个指向虚表开始的虚指针,占4/8字节;

虚表与类对应;虚表指针和对象对应

定义一个函数为虚函数,是为了允许基类指针调用子类的这个函数

多态

不同对象调用相同的函数,但呈现多样的结果

分类

编译时多态

编译过程中静态确定同名操作与具体对象绑定的关系(函数重载、运算符重载、模板)

优点:程序运行时函数调用速度快、效率高

缺点:编程不灵活

运行时多态

动态确定同名操作与具体对象绑定的关系(继承和虚函数)

优点:编程更加灵活、系统易于扩展

缺点:调用速度比静态绑定的函数慢

条件:

1.有虚函数;

2.符合赋值兼容规则;

3.由指针或引用去调用虚函数

如果不用虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class test1{
protected:
int b;
public:
test1(int v):b(v) {}
// virtual void eat()=0;
void eat(){
cout<<"test1"<<endl;
}
};
class him:public test1{
public:
him():test1(0){}
// virtual void eat() override{
// cout<<"him,"<<b<<endl;
// }
void eat() {
cout<<"him,"<<b<<endl;
}
};
class you:public test1{
private:
int c;
public:
you():test1(1),c(2){}
// virtual void eat() override{
// cout<<"you,"<<c<<endl;
// }
void eat() {
cout<<"you,"<<c<<endl;
}
};
int main(){

you* pyou=new you();
him* phim=new him();

//基类的指针指向派生类的对象
test1* eater1=(test1*)pyou;
test1* eater2=(test1*)phim;

eater2->eat();
eater1->eat();
return 0;
}

test1
test1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using namespace std;
class test1{
protected:
int b;
public:
test1(int v):b(v) {}
virtual void eat()=0;
};
class him:public test1{
public:
him():test1(0){}
virtual void eat() override{
cout<<"him,"<<b<<endl;
}
};
class you:public test1{
private:
int c;
public:
you():test1(1),c(2){}
virtual void eat() override{
cout<<"you,"<<c<<endl;
}
};
int main(){

you* pyou=new you();
him* phim=new him();

//基类的指针指向派生类的对象
test1* eater1=(test1*)pyou;
test1* eater2=(test1*)phim;

//相同结果
// you pyou;
// him phim;

// test1* eater1= &pyou;
// test1* eater2= &phim;

eater2->eat();// 调用 him 类的 eat() 函数
eater1->eat();//// 调用 you 类的 eat() 函数
return 0;
}

him,0
you,2

有了虚函数指定哪个对象,就调用哪个函数的方法

类->内存对象->虚函数表

PS:那个override可写可不写,c++11提出的,为了增加可读性

基类函数写了virtual,派生类就不需要写了,重写一遍函数就行了

析构函数

指在delete对象指针的时候,调用的函数

1
2
3
4
5
Base *bp;

bp=new derived();//Base的派生

delete bp;

如果基类没有加virtual,delete时只返回基类的析构

如果基类加了virtual,派生的析构可不加,delete时返回基类+派生的析构

总结

纯虚函数

而且基类一般不需要实现virtual 函数,它只为派生提供一个接口,只用提供函数名,而不实现函数体,即为纯虚函数virtual void eat()=0;(声明语句,而不能被调用)

纯虚函数的类叫做抽象类,不能实例化一个抽象类的对象,所以会报错,因为没有具体的功能实现

如果其派生类也不实现同名函数的话(比如,直接不写eat()函数)也会报错的。

纯虚函数的优点就是可以防止派生类忘记实现虚函数

虚函数

虚函数的使用:

第一步,基类,声明某个基类函数为virtual

第二步。派生类,定义在基类的virtual函数

第三步,声明基类指针,指向派生类,调用virtual函数

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table) 来实现的。简称为 V-Table 。在这个表中,主是要一个类的虚 函数的地址表,这张表解决了继承、覆盖的问题,保证其容 真实反应实际的函数。

接口

在面向对象程序设计中,创建一个只包含未实现方法然后交由子类去实际实现功能的类是非常普遍的,这通常被称为接口接口就是一个只包含未实现的方法并作为一个模板的类。并且由于此接口类实际上不包含方法实现,所以我们无法实例化这个类。

比如来个打印类名的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class printable{//接口,就纯模板
public:
virtual string printclass()=0;
};
//基类
class entity:public printable{
public:
virtual string getname(){return "entity";}
string printclass() {return "entity";}//实现接口的纯需函数

};
//派生类
class player:public entity{//继承entity,entity已有接口
private:
string pname;
public:
player(const string& name):pname(name){}
string getname() {return pname;}
string printclass() {return "player";}//实现接口的纯虚函数

};

void print(printable *obj){//为了输出返回值为string类型的函数,它接收printable对象,不关心什么类
cout<<obj->printclass()<<endl;
}
int main(){
entity* e=new entity();
// player* p=new player("132");
entity*p =new player("132");
print(e);
print(p);
return 0;
}

entity
player

PS:如果player不继承entity

1
2
3
4
class player : public Otherclass,printable{
...
string pintclass() {return "player";}
};

这个就体现了下接口。

当然啊,如果还是看虚函数的话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//基类
class entity:public printable{
public:
//virtual string getname(){return "entity";}
string getname(){return "entity";}
string printclass() {return "entity";}//实现接口的纯需函数

};
...
entity* e=new entity();
// player* p=new player("132");
entity*p =new player("132");
// print(e);
// print(p);
cout<<e->getname();
cout<<endl;
cout<<p->getname();

entity
entity

但是:基类函数加了virtual后嘞

entity
132

2.C++中 const、define、static 的区别?

const

const 是单词 constant 的简写,字面意思是常数、常量。 用于变量修饰,表明这个变量不能被修改;(mutable就是可改)

用于指针修饰, 表明指针的指向物不能被修改

用于方法修饰,表明这个方法不会对对象造成改变。

const修饰指针时,cosnt位置不同,那么修饰对象不同

int *const p2中 const修饰的是p2的值,即该指针p2的指向不可变,但*p2可读取该指向的变量值

int const *p1与const int *p1中,const修饰*p1,即*p1的值不可改变,但可以更改p1的指向

底层指针:指示指针所指向的变量是一个常量

define

#define 和另外两个不一样,它属于,是预处理器的一部分。

预处理是在编译之前的一道,简单地进行字符串替换,它不按照语言的语法,而是直管自己的语法。

定义的常量没有类型;而const定义的常量有类型名字,且放在静态区域

#define定义的常量不可以用指针指向;const可以

#define可以定义简单的函数,const不可以定义函数

static

static 的定义相对较复杂,用在全局变量表明这个变量在每个编译单元有独自的实例;

用在函数里的局部变量,表明它的生存周期其实是全局变量,但仅在函数内可见;(可用来统计函数调用次数)

用在类成员,表明成员或者方法是类的,而不是对象实例的。

静态成员不占用类的大小,普通函数也不占用

作用

  • 隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性
  • 保持变量内容的持久:存储在静态数据 区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
  • 默认初始化为0(static变量);全局变量也具备这一属性,因为全局变量也存储在静态数据区。
  • c++类成员声明:
    • 函数体内static变量的作用范围为该函数体
    • 模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问
    • 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
    • static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;

*3. 对于epoll底层结构和原理有什么了解?

这啥啊Σ(⊙▽⊙”a

eventpoll的使用中,经常需要对文件描述符集合进行添加、删除等操作,同时对触发的事件类型进行处理,回调IO事件中的工作函数。

epoll,I/O多路复用技术,最大特点是支持高并发,从linux内核2.6引入的

头文件#include <sys/epoll.h>

三个关键函数

创建 eventpoll 对象

int epoll_create(int size);

操作 eventpoll 对象

int epoll_ctl(int epfd, int op, int fd, struct epoll_events* event);

从 eventpoll 对象中返回活跃的事件。

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

epoll_create;

  • 功能:创建一个epoll对象,返回该对象的描述符【文件描述符】,这个描述符就代表这个epoll对象,后续会用到。
  • 这个epoll对象最终要用close(),因为文件描述符/句柄 总是关闭的。
  • size > 0;。

epoll_ctl

  • 功能:把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的就是通过这个epoll对象来监视这个socket【客户端的TCP连接】上数据的来往情况;当有数据来往时,系统会通知我们。
  • 我们把感兴趣的事件通过 epoll_ctl() 添加到系统,当这些事件来的时候,系统会通知我们。
  • efpd:epoll_create()返回的epoll对象描述符
  • op:动作,添加/删除/修改 ,对应数字是1,2,3, EPOLL_CTL_ADD, EPOLL_CTL_DEL ,EPOLL_CTL_MOD
  • EPOLL_CTL_ADD添加事件:等于你往红黑树上添加一个节点,每个客户端连入服务器后,服务器都会产生一个对应的socket,每个连接这个socket值都不重复,所以,这个socket就是红黑树中的key,把这个节点添加到红黑树上去
  • EPOLL_CTL_MOD:修改事件,用了EPOLL_CTL_ADD把节点添加到红黑树上之后,才存在修改
  • EPOLL_CTL_DEL:是从红黑树上把这个节点干掉这会导致这个socket【这个tcp链接】上无法收到任何系统通知事件
  • sockid:表示客户端连接,就是你从accept()这个是红黑树里边的key;
  • event:事件信息,这里包括的是 一些事件信息EPOLL_CTL_ADD和EPOLL_CTL_MOD都要用到这个event参数里边的事件信息

epoll_wait

  • 功能:阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知

与select相比

网络编程中,当每个线程都要阻塞在 recv 等待对方的请求,如果访问的人多了,线程开的就多了,大量线程都在阻塞,系统运转速度也随之下降。这个时候,你需要多路复用技术。

select不能应付海量的网站访问,所以需要epoll

  • select底层采用数组来管理套接字描述符,管理的数量有上限,不超过几千个;

    而epoll使用树和链表来管理,同时管理数量可以更大

  • select不会告诉你到底哪个套接字来了消息,需要轮询;

    epoll直接告诉谁来了消息

  • select进行系统调用,需要把套接字列表在用户空间和内核空间来回拷贝;

    epoll统一在内核管理套接字描述符,无需来回拷贝

来源

CSDNNGC_2070

4. epoll 的 ET 模式和 LT 模式哪个更高效?

LT(level triggered) 默认/缺省的工作方式,同时支持 block和no_block socket。这种工作方式下,内核会通知你一个fd是否就绪,然后才可以对这个就绪的fd进行I/O操作。就算你没有任何操作,系统还是会继续提示fd已经就绪,不过这种工作方式出错会比较小,传统的select/poll就是这种工作方式的代表。

ET(edge-triggered) 是高速工作方式,仅支持no_block socket,这种工作方式下,当fd从未就绪变为就绪时,内核会通知fd已经就绪,并且内核认为你知道该fd已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个fd进行I/O操作,导致fd变为未就绪时,内核同样不会发送更多的通知,因为only once。所以这种方式下,出错率比较高,需要增加一些检测程序。

PS:fd(文件描述符)

LT可以理解为水平触发,只要有数据可以读,不管怎样都会通知。而ET为边缘触发,只有状态发生变化时才会通知,可以理解为电平变化

来源编程哲学家

所以嘞:

ET 模式更加高效。
与 poll 的事件宏相比, epoll 新增了一个事件宏 EPOLLET , 这就是所谓的边缘触发模式(
Edge Trigger , ET ),而默认的模式称为水平触发模式(Level Trigger, LT )。 对于水平触发模式,一个事件只要有,就会一直触发; 对于边缘触发模式,只有一个事件从无到有才会触发。

CSDN博主「丘比特惩罚陆」

5.什么情况下C++STL迭代器会失效?

当容器调用 erase() 方法后,当前位置到容器末尾元素的所有迭代器全部失效。 当容器调用 insert() 方法后,当前位置到容器末尾元素的所有迭代器全部失效。 如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了。

6.什么是右值引用?和移动语义、完美转发有什么联系?

它是移动语义和完美转发的基石,定义右值引用需要使用&&、右值引用一定不能被左值所初始 化,只能用右值初始化。

左值

有地址、数值、有存储空间的值,长期存在

左值是由某种存储支持的变量,左值有地址和值,可以出现在赋值运算符左边或右边

左值引用

左值引用仅仅接收左值,除非用了const兼容(非const的左值引用只接受左值),所以c++常用常量引用。

它们兼容临时的右值和实际存在的左值变量

例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(){
int x=8;
int& refx=x;
cout<<"x: "<<x<<endl;
cout<<"refx: "<<refx<<endl;

refx=28;

cout<<"x: "<<x<<endl;
cout<<"refx: "<<refx<<endl;
return 0;
}

x: 8
refx: 8
x: 28
refx: 28

在这个示例中,refx 是一个引用,它引用了变量 x。左值引用允许你通过 refx 修改 x 的值,因为引用绑定后,对引用的修改实际上是对被引用变量的修改。

需要注意的是,一旦引用绑定到某个变量,它会一直引用该变量,不能重新绑定到其他变量。因此,左值引用一般在绑定后不会再改变其引用对象。

左值引用在 C++ 中用于多种目的,例如通过函数引用传递变量、运算符重载以及为现有变量创建别名。

右值

就是临时量,无地址(或者有地址,但访问不到,只是一个临时量),没有存储空间而短暂存在的值

右值引用

c++11出现的

右值引用不能绑定到左值,可以通过常引用或右值引用延长右值的生命周期。

“有名字的右值引用”是左值

定义需要使用&&

1
int&& rref = 42; // rref 是一个右值引用,绑定到临时右值 42

rref 是一个右值引用,它可以绑定到一个临时右值,如字面值或表达式计算的结果。右值引用通常用于实现移动语义和完美转发(perfect forwarding),允许有效地管理资源并将参数传递到其他函数,同时避免不必要的数据复制。

需要注意的是,与左值引用不同,右值引用可以绑定到临时对象,但不能绑定到具有名称的左值。右值引用通常在函数参数中使用,例如在移动构造函数和移动赋值运算符中。

1 string foo( ); 2 void bar(string & s); 为什么下面的操作非法1 bar(foo( )); 2 bar(“hello world”);

  1. Rvalue vs. Lvalue: 在 C++ 中,表达式可以被分为右值(Rvalue)和左值(Lvalue)。右值是临时的、不可修改的值,左值是可寻址的、可修改的值。函数返回的临时对象通常是右值,而具有名称的变量是左值。在 bar(foo()); 中,foo() 返回一个临时的 string 对象,因此它是一个右值。

  2. Const Reference and Non-Const Reference: 函数参数可以是 const 引用(const reference)或非 const 引用。string& s 表示非 const 引用,而 const string& s 表示 const 引用。

  3. bar(foo());: 在这个操作中,foo() 返回的是右值,但 bar 函数的参数要求是非 const 引用,而右值不能直接绑定到非 const 引用,因此这个操作是非法的。

  4. bar("hello world");: "hello world" 是一个字符串字面量,它是右值,而 bar 函数的参数要求是非 const 引用,同样不能直接绑定到非 const 引用,因此这个操作也是非法的。

解决:

  1. 修改 bar 函数参数为 const string& s,以允许将右值(临时对象)绑定到 const 引用参数。
  2. foo 调用后创建一个命名的 string 变量,然后将这个变量传递给 bar 函数,因为左值可以绑定到非 const 引用参数。
1
2
3
4
5
6
7
8
9
10
11
void bar(const string& s);

int main() {
string fooResult = foo();
bar(fooResult); // 合法,将左值传递给非 const 引用参数

bar("hello world"); // 合法,将右值传递给 const 引用参数

return 0;
}

总结

可以取地址的,有名字的就是左值

不能取地址的,没名字的就是右值

int a=b+c;//a左值,变量名为a,&a为其地址;b+c表达式、函数int func()返回值是右值

移动语义

c++11后出现的

意义

有时候我们需要单纯创建一些右值,赋给某个对象用作构造函数。

这个时候需要:

在main函数里创建这个右值对象,然后复制给这个对象相应的成员变量。

这里有一个额外行为,是复制

如果我们可以直接把这个右值变量移动给成员变量,程序性能就会提高

所以我们要移动对象,而不是复制它,这就有了语义的出现。

看一下复制的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>
#include<string.h>//strlen的头文件
using namespace std;
class String{
private:
char * m_data;
uint32_t m_size;
public:
String()=default;
String(const char* str)//构造函数
{
printf("Created!\n");
m_size=strlen(str);
m_data=new char[m_size];
memcpy(m_data,str,m_size);
}
String(const String& other)//拷贝构造函数
{
printf("Copyed!\n");
m_size=other.m_size;
m_data=new char[m_size];
memcpy(m_data,other.m_data,m_size);
}
~String()
{
delete m_data;
}
void print()
{
for (uint32_t i = 0; i < m_size; i++)
{
printf("%c",m_data[i]);
}

printf("\n");
}
};

class entity{
private:
String m_name;
public:
entity(const String& name):m_name(name)
{
}
void printname()
{
m_name.print();
}
};
int main(){

entity e(String("yuleiyun"));
e.printname();
return 0;
}

Created!
Copyed!
yuleiyun

1
2
3
4
5
6
7
String(const String& other)//拷贝构造函数
{
printf("Copyed!\n");
m_size=other.m_size;
m_data=new char[m_size];
memcpy(m_data,other.m_data,m_size);
}

复制字符串,意味着我们需要在堆上分配内存,调用新字符

我们的主函数中写入了参数,然后调用的是

1
2
3
entity(const String& name):m_name(name)
{
}

为什么不能直接写进

1
2
private:
String m_name;

这就引入了移动语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class String{
private:
char * m_data;
uint32_t m_size;
public:
String()=default;
String(const char* str)//构造函数
{
printf("Created!\n");
m_size=strlen(str);
m_data=new char[m_size];
memcpy(m_data,str,m_size);
}
String(const String& other)//拷贝构造函数
{
printf("Copyed!\n");
m_size=other.m_size;
m_data=new char[m_size];
memcpy(m_data,other.m_data,m_size);
}
String(String&& other) noexcept//右值引用拷贝,相当于移动(复制一次指针,原来的指针给nullptr)
{

printf("Moved!\n");
//新对象的指针指向指定内存,将旧对象的指针移开
//这里是接管了原来的旧内存
m_size=other.m_size;
m_data=other.m_data;

//数据的转移,将other的数据偷走
other.m_size=0;
other.m_data=nullptr;

}
~String()
{
printf("Desdroyed\n");
delete m_data;
}
void print()
{
for (uint32_t i = 0; i < m_size; i++)
{
printf("%c",m_data[i]);
}

printf("\n");
}
};

class entity{
private:
String m_name;
public:
entity(const String& name):m_name(name)
{
}
entity(String&& name):m_name(move(name))//或者(String&&)name
{

}
void printname()
{
m_name.print();
}
};
int main(){

entity e(String("yuleiyun"));
e.printname();
return 0;
}

Created!
Moved!
Desdroyed
yuleiyun
Desdroyed

String(String&& other) noexcept

&&语法用于定义移动构造函数,它是一种特殊类型的构造函数,允许将资源(如动态分配的内存)从一个对象有效传输到另一个对象。

在 C++ 中,移动构造函数是使用右值引用作为参数来定义的。&&表示这是一个右值引用参数,并且它允许在从右值(临时)对象构造对象或显式转换为右值引用时调用移动other构造函数。

当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如节省拷贝之类的。 move的作用是无条件的把表达式转成右值

来源 知乎

7.智能指针定义、类别和应用场景

作用是管理一个指针。

存在以下这种情况:

申请的空间在函数结束时忘记释放,造成内存泄漏。通过智能指针避免该问题。

智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。

其作用原理就是:在函数结束时自动释放内存空间,不需要手动释放内存空间。

unique_ptr独占式,同一时间只允许一个智能指针指向该对象

shared_ptr共享式,多个智能指针指向同一个对象,当最后一个引用被销毁时,就会释放对象和其相关资源

weak_ptr 是一种不控制对象生命周期的智能指针 , 它指向 一个 shared_ptr 管理的对象 . 进行该对象的内存管理的是那个强引用的 shared_ptr , weak_ptr 只是提供了对管理对象的一个访问手段。

定义

本质上是原始指针的包装。当你创建一个智能指针,它会调用new并为你分配内存,然后基于你使用的智能指针,这些内存会在某一时刻自动释放。

优先使用unique_ptr,其次考虑shared_ptr

尽量使用unique_ptr因为它有一个较低的开销,但如果你需要在对象之间共享,不能使用unique_ptr的时候,就使用shared_ptr

访问所有智能指针,先要有头文件<memory>

作用域指针unique_ptr的使用

  • 作用域指针意味着,超出作用域,就会被销毁,调用delete

  • 唯一、不可复制、不可共享

如果复制一个unique_ptr,会有两个指针,两个unique_ptr指向同一个内存块,如果其中一个死了,它会释放那段内存,也就是说,指向同一块内存的第二个unique_ptr指向了已经被释放的内存。

  • unique_ptr 需要显示调用构造函数
  • 最好使用 unique_ptr<Entity> entity=make_unique<Entity>();以防构造函数抛出异常,得到一个没有引用的空指针而造成内存泄露,稍安全点
  • make_unique<>() c++14引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<memory>
using namespace std;
class Entity{
private:

public:
Entity()
{
std::cout << "Create!" << std::endl;
}

~Entity()
{
std::cout << "Destroy!" << std::endl;
}
void Print(){}
};
int main()
{

//unique_ptr<Entity> entity = new Entity(); //错误,unique_ptr不能隐式转换
//unique_ptr<Entity> entity(new Entity()); //可以,但不建议说是
unique_ptr<Entity> entity=make_unique<Entity>();
entity->Print();//像一般原始指针的使用方式

return 0;
}

Create!
Destroy!

共享指针shared_ptr

  • 其工作方式 通过引用计数

引用计数基本上是一种方法,可以跟踪你的指针有多少个引用,一旦引用计数达到零,他就被删除了。
例如:我创建了一个共享指针shared_ptr,我又创建了另一个shared_ptr来复制它,我的引用计数是2,第一个和第二个,共2个。当第一个死的时候,我的引用计数器现在减少1,然后当最后一个shared_ptr死了,我的引用计数回到零,内存就被释放。

  • 该指针需要分配另一块内存,叫控制块,存储引用计数

首先创建一个new Entity,然后传递给shared_ptr构造函数,它必须分配两次;

用make_shared组合起来更有效率、

1
2
std::shared_ptr<Entity> sharedEntity = sharedEntity(new Entity());//不推荐!
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();//ok
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

int main()
{

{
std::shared_ptr<Entity> e;
{
// std::shared_ptr<Entity> sharedEntity = sharedEntity(new Entity());//不推荐!
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();//ok
e = sharedEntity; //可以复制
} //此时sharedEntity已经“死了”,但没有调用析构,因为e仍然是活的,并且持有对该Entity的引用,此时计数由2-》1
} //析构被调用,因为所有的引用都消失了,计数由2->0,内存被释放


std::cin.get();
}

Create!
Destroy!

注:main函数的花括号是为了让括号内的内容为局部对象,方便测试

弱指针weak_ptr

  • 可以和shared_ptr一起使用
  • 该指针可以被复制,但 不会增加额外的控制块来计数,仅仅声明该指针活着

当你将一个shared_ptr赋值给另外一个shared_ptr,引用计数++,而若是把一个shared_ptr赋值给一个weak_ptr时,它不会增加引用计数。这很好,如果你不想要Entity的所有权,就像你可能在排序一个Entity列表,你不关心它们是否有效,你只需要存储它们的一个引用就可以了。

1
2
3
4
5
6
7
8
...
{
std::weak_ptr<Entity> e0;
{
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
e0 = sharedEntity;
} //此时,此析构被调用,内存被释放
}

它指向一个 shared_ptr 管理的对象 . 进行该对象的内存管理的是那个强引用的 shared_ptr , weak_ptr 只是提供了对管理对象的一个访问手段。

8.vector 和 list 有什么区别?

vector 和 built-in 数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[] 操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。

它的emplace_back()体现了零拷贝技术,虽然后push_back()作用一样,但不需要调用拷贝构造函数和转移构造函数,而是原地构造该元素

list 就是数据结构中的双向链表,它的内存空间可以是不连续的,通过指针来进行数据的访问, 这个特点使得它的随即存取变的非常没有效率,因此它没有提供[]操作符的重载。支持任意地方的删除和插入

vector 适用:对象数量变化少,简单对象,随机访问元素频繁;

list 适用:对象数量变化大,对象复杂,插入和删除频繁

list

C++ STL中提供的一种容器,是线性的双向链表的数据结构。拥有链式结构的特征支持元素的快速插入和删除,但是元素随机访问较慢(相较于vector容器),不提供[]运算符的重载。C++中使用list容器需要包含头文件,把list当做双向链表来看

基本操作

push_back()

push_front();

pop_

merge()//两个list表必须事先有序

sort()

vector

向量

与数组相比其优点在于它能够根据需要随时自动调整自身的大小以便容下所要放入的元素。

插入 - insert

​ ①、a.insert(a.begin(), 1000);//将1000插入到向量a的起始位置前

​ ②、a.insert(a.begin(), 3, 1000);//将1000分别插入到向量元素位置的0-2处(共3个元素)

​ ③、 vector<int> a(5, 1) ;

​ vector<int> b(10) ;

​ b.insert(b.begin(), a.begin(), a.end()) ; //将a.begin(), a.end()之间的全部元素插入到b.begin()前

删除 - erase

​ ①、b.erase(b.begin());//将起始位置的元素删除

​ ②、b.erase(b.begin(), b.begin()+3);//将(b.begin(), b.begin()+3)之间的元素删除

交换 - swap

​ b.swap(a) ; //a向量与b向量进行交换

9. C++内存 new 与 malloc 区别是什么?

  1. 申请的内存所在位置;

    new:此操作符分配的内存空间是在自由存储区;

    malloc:申请的内存是在堆空间。

    ​ 是C语言和操作系统的术语,堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,调用free()归还内存。

    自由存储区

    ​ 是C++中动态分配和释放对象的一个概念,通过new分配的内存区域可以称为自由存储区,通过delete释放归还内存。自由存储区可以是堆、全局/静态存储区等,具体是在哪个区,主要还是要看new的实现以及C++编译器默认new申请的内存是在哪里。但是基本上,很多C++编译器默认使用堆来实现自由存储,运算符new和delete内部默认是使用malloc和free的方式来被实现,说它在堆上也对,说它在自由存储区上也正确。因为在C++中new和delete符号是可以重载的,我们可以重新实现new的实现代码,可以让其分配的内存位置在静态存储区等。而malloc和free是C里的库函数,无法对其进行重载。

    CSDN博主「呆萌理科生」

  2. 返回类型安全性;

    new分配内存成功,则返回对象类型的指针,使用new是类型安全的;而malloc分配成功会返回void*类型指针,需要通过强制类型转换

  3. 内存分配失败时的返回值;

    C++标准中new如果分配内存失败要求抛出bad_alloc异常,需要自己捕捉,但是有些编译器对C++标准的适配没那么好,也会返回null指针,这是保留了C的处理。

    而malloc分配失败则会返回null指针。

  4. 是否需要指定的内存大小;

    使用malloc为对象指针分配内存,要明确指定分配内存的大小,而new不需要

    malloc返回的指针还需要进行强制类型转换才赋值给A的指针对象,而使用new则不需要,直接返回的是A的指针。

  5. 是否调用构造函数/析构函数;

    new调用时先为对象分配内存,再调用对象的构造函数,delete会调用析构函数

    而malloc不会

    A* a=new A();

    A* aa=(A*)malloc (sizeof(A));

  6. 对数组的处理;

    new的话,有new [],delete []

    A * ptr = new A[10];//分配10个A对象

    delete [] ptr;

    ——————————

    int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组

  7. 是否可以被重载;

    new 是操作符(需要编译器支持),可以被重载,malloc/free是库函数(头文件支持),不能被重载

  8. 能够直观的重新分配内存;

    malloc内存不够,可以用realloc扩张内存大小

    new没有

  9. 客户处理内存分配不足;

总结:

malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种

而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施给你使用

来源:CSDN AI浩

有了malloc/free 为什么还要有new/delete

new 在对象创建的时候执行构造函数/ delete 销毁的时候执行析构函数

new的工作原理:

先调用operator new函数,申请足够内存(底层用malloc)

调用类型的构造函数,初始化成员变量

返回自定义类型指针

new[] 返回的是元素类型的指针

new的机制是将内存分配和对象构造组合一起

delete工作原理

调用operator delete释放内存(底层用free)

delete[] 将元素逆序销毁

delete也是将对象析构和内存释放组合一起

而allocator将这两部分分开,申请一部分内存,不初始化对象,只有当需要的时候才初始化操作

被free回收的内存是立即返回操作系统吗

否,会首先被ptmalloc使用双链表保存起来,并尝试对小块内存合并,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回,就避免了频繁的系统调用,占用过多的系统资源。

10.常量指针和指针常量有什么区别?

指向const的指针,指向一个常量项目。指针指向的数据不能改变,但指针本身可以改变

对于const指针,指针本身就是常量。一旦指针使用了某个地址进行初始化,那么它就不能指向除此地址之外的任何其他东西

来自 精通c++(第9版)

指针常量

指针常量是一个指针,(即指向常量的指针)指向一个只读常量,这个值因为是常量,所以不能被修改,const在*之前

const int* p //const int是p指向的内容,*表示p是一个指针

注意的是:const适用于p指向的东西;而不是p本身

虽然常量的地址只能传递给指向const的指针,但是,指向const的指针是可以接收非常量的

1
2
3
4
5
6
7
8
9
10
11
12
void display(const int *nums,int size)
{
for(int i=0;i<sizel;i++)
cout<<*(nums+i)<<" ";
}
...
int size=6;
const int arr1[size]={1,2,3,4,5,6};
display(arr1,size);// 1 2 3 4 5 6

int arr2[size]={2,4,6,8,10,12};
display(arr2,size);//2 4 6 8 10 12

应用场景:当编写一个使用指针形参的函数,并且该函数不打算改变形参指向的数据时,将形参设置为一个指向const的指针。

补充:

数组名称是指针常量(Pointer Conster),不能让它们指向除了它们所代表的数组之外的任何东西

int a[20],b[20];

int *p;

p=a;//正确

a=b;//错误

1
2
3
4
5
6
int x = 10; 
int y = 20;
const int* ptr = &x;
ptr = &y;
//*x=30 //Error
它可以指向不同的常量,但数据不能改

常量指针

常量指针是一个不能给改变指向的指针。指针是一个常量必须初始化,一旦初始化完成,它的值,即这个地址就不能改变,不能中途改变指向 ,const在*之后,如

int *const p; //*const 表示p是一个常量指针,int是p指向的内容

1
2
3
4
5
6
7
int x = 10;
int y = 20;

int* const ptr = &x; // ptr is a pointer constant to an integer, pointing to x
//ptr = &y; // Error
*x = 30; // Valid
它可以修改数据,但不能修改指向

补充

还有一个指向常量的常量指针,const int *const ptr;//const int是ptr指向的内容,*const表示ptr是一个常量指针 既不可以改变指针指向的内容,也不能再去修改指针指向的地址

11. map 和 unordered_map 的区别以及适用场景?

map会自动按键的字典序排序

unordered_map随机排序,用于查找

12. 静态链接库和动态链接库各自的优缺点分别是什么?

静态链接库的优点:

代码装载速度快,执行速度略比动态链接库快;

只需保证在开发者的计算机中有正确的.LIB 文件,在以二进制形式发布程序时不需考虑在用户的计算机
上 .LIB 文件是否存在及版本问题,可避免 DLL 地狱等问题。

​ 缺点:

使用静链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费

全量更新

动态链接库优点:

更加节省内存并减少页面交换;

DLL 文件与 EXE 文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换 DLL 文件不会对 EXE 文件造成任何影响,因而极大地提高了可维护性和可扩展性;

不同编程语言编写的程序只要按照函数调用约定就可以调用同一个 DLL 函数;

缺点:

使用动态链接库的应用程序不是自完备的,它依赖的 DLL 模块也要存在,如果使用载入时动态链接, 程序启动时发现 DLL 不存在,系统将终止程序并给出错误信息。

而使用运行时动态链接,系统不会终止,但由于 DLL 中的导出函数不可用,程序会加载失败;

速度比静态链接慢

当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统撕掉。

知识点

库是写好的现有的,成熟的,可以复用的代码。

静态和动态指链接

将一个程序编译成可执行程序的步骤:将源文件(.h,.cpp)预编译->编译->汇编->链接

静态库是.lib

其特点:

  1. 静态库对函数库的链接是放在编译时期完成的
  2. 程序在运行时与函数库再无瓜葛,移植方便
  3. 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件

动态库是.dll

由于静态库的空间浪费和全量更新(如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户)

其特点:

  1. 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。
  2. 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题
  3. 解决了静态库对程序的更新、部署和发布页会带来麻烦

来源CSDN hllyzms

13. C++虚函数、纯虚函数的区别和适用场景?

虚函数:

在类成员方法的声明语句前加 “virtual”, 如 virtual void func() ;

对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称之为复写 Override 。

纯虚函数:

在虚函数后加“=0” ,如 virtual void func()=0 ;

对于纯虚函数,子类必须提供纯函数的个性化实现

使用场景:当子类必须要实现个性化的时候,用纯虚函数

14. 是否遇到过 C++变量冲突的情况?如何解决?

换变量名;

应用命名空间;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//来自chatgpt

#include <iostream>

// First namespace named 'A'
namespace A {
int num = 5; // Variable 'num' in namespace 'A'
}

// Second namespace named 'B'
namespace B {
int num = 10; // Variable 'num' in namespace 'B'
}

int main() {
// Accessing variables from namespaces
std::cout << "Value from namespace A: " << A::num << std::endl;
std::cout << "Value from namespace B: " << B::num << std::endl;

return 0;
}

命名空间嵌套;

1
2
3
4
5
6
7
8
9
10
11
namespace Outer {
int num = 5; // Variable 'num' in outer namespace 'Outer'

// Inner namespace named 'Inner' within 'Outer'
namespace Inner {
int num = 10; // Variable 'num' in inner namespace 'Inner'
}
}
...
std::cout << "Value from outer namespace: " << Outer::num << std::endl;
std::cout << "Value from inner namespace: " << Outer::Inner::num << std::endl;

加入类前缀 作为成员变量

15 .聊聊 C++的内存碎片?

即碎片的内存,分为外碎片和内碎片

内存碎片指的是系统中不可用的空闲内存

由于空闲内存小,且以不连续方式出现在不同位置。

该问题解决取决于内存管理算法

16. C++中 lambda 表达式的捕捉变量的方式有哪几种?

lambda

lambda本质上是一个匿名函数。 用这种方式创建函数不需要实际创建一个函数 ,它就像一个快速的一次性函数 。 lambda更像是一种变量,在实际编译的代码中作为一个符号存在,而不是像正式的函数那样。

使用格式:[]( {参数表} ){ 函数体 }

中括号表示的是捕获,作用是如何传递变量 lambda使用外部(相对)的变量时,就要使用捕获

如果使用捕获,则:

  • 添加头文件: #include <functional>
  • 修改相应的函数签名 std::function <void(int)> func替代 void(*func)(int)
  • 捕获[]使用方式:

[=],则是将所有变量值传递到lambda中
[&],则是将所有变量引用传递到lambda中
[a]是将变量a通过值传递,如果是[&a]就是将变量a引用传递
它可以有0个或者多个捕获

基础使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<vector>

void foreach(const std::vector<int> &values,void(*func)(int))
{
for(int value:values)
func(value);
}
int main(){

std::vector<int> values={6,1,7,0,9};
foreach(values,[](int value){std::cout<<"Value: "<<value<<std::endl;});
return 0;
}

Value: 6
Value: 1
Value: 7
Value: 0
Value: 9

这里对捕获解释一下

1
2
3
4
std::vector<int> values={6,1,7,0,9};
int a=5;
auto lambda=[](int value){std::cout<<"Value: "<<a<<std::endl;};
foreach(values,lambda);

看,我们尝试让lambda函数输出a

error: ‘a’ is not captured
auto lambda=[](int value){std::cout<<”Value: “<<a<<std::endl;};
^
lambda.cpp:13:18: note: the lambda has no capture-default
auto lambda=[](int value){std::cout<<”Value: “<<a<<std::endl;};
^
lambda.cpp:12:9: note: ‘int a’ declared here
int a=5;

报错说,a没有捕获

在中括号补充一个=

1
auto lambda=[=](int value){std::cout<<"Value: "<<a<<std::endl;};

error: cannot convert ‘main()::<lambda(int)>’ to ‘void ()(int)’ for argument ‘2’ to ‘void foreach(const std::vector<int>&, void ()(int))’
foreach(values,lambda);

因为使用了捕获

所以要添加头文件,然后更改下新的函数指针的签名

1
2
3
4
5
6
7
8
9
10
..
#include<functional>
void foreach(const std::vector<int> &values,const std::function<void(int)>& func)
{
for(int value:values)
func(value);
}
...
auto lambda=[=](int value){std::cout<<"Value: "<<a<<std::endl;};
//这里[&] [a] [&a]都可

Value: 5…

万一我们想对捕获的变量修改值怎么办呢?

需要用到mutable

1
2
3
...
auto lambda=[=](int value) mutable {a=6,std::cout<<"Value: "<<a<<std::endl;};
...

Value: 6…

还可以写一个lambda去接受vector元素,遍历该vector找到大于6的数,并返回它的迭代器

find_if是搜索类函数,需要头文件algorithm

它与find不同,find_if是可以接受一个函数指针来定义搜索的规则,返回满足这个规则的第一个元素的迭代器

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<vector>
#include<algorithm>
int main(){

std::vector<int> values={6,1,7,0,9};
auto it=find_if(values.begin(),values.end(),[](int value){return value>6;});
std::cout<<*it;
return 0;
}

7

17.C++中 final 关键字是做什么的?能否想到更多作用?

C++11 引入了关键字 final ,按官方的标准是该关键字是用来标识虚函数不能在子类中被覆盖(override) ,或一个类不能被继承。

18.对 RPC 框架有什么了解?原理?

远程过程调用(Remote Procedure Call ,缩写为RPC)是一个计算机通讯协议

允许运行于一台计算机的程序调用另一台计算机的子程序,就像调用本地程序一样,无需额外的为这个交互作用编程(无需关注细节)

RPC用户调用接口 + 具体网络协议。 前者为开发者需要关心的,后者由框架来实现。

RPC是一种服务器-客户端模式,经典实现是一个通过 发送请求-接收响应 进行信息交互的系统

引入

如果有一个数独求解服务器,包含了登陆服务模块、计算求解模块、后台管理模块,开始时服务器可以部署在单机,但是如果用户增多、请求计算量增多,那么就需要集群分布式部署架构达到高并发要求

集群

将服务器部署到了多台机器上,每一台机器上都是独立的服务器。

再通过负载均衡服务器进行管理,供用户使用,由此大大增加了服务器的并发负载量。

这种部署方式比较简单,并且切实的提高了服务器的并发量。

缺点:

1.服务器的一个模块修改,所有服务器都要重新编译、部署,所以是维护麻烦

2.服务器有些模块是CPU密集型,需要CPU性能强的机器,有些I/O密集型,需要内存更大点的机器,所以是各模块对硬件资源要求不同

因此,引入分布式架构

分布式

支持分模块部署服务器,例如将数独服务器的计算模块部署到一个CPU强的机器上(不一定是一个,可以单独对模块进行集群部署),将登录模块部署到内存大的机器上。

如此分模块运行在不同机器的docker虚拟化环境中,属于不同的进程,分布在不同机器的各个模块共同构成了高性能的服务器。由此,集群的两个问题都可以得到解决

但是嘞,有这样一种情况:有些模块的某段代码,其他模块也需要调用,这样的话,模块间是有大量重复代码

为了让机器1上的模块调用机器2上模块的方法,必要要有一个通信协议,即RPC通信框架

RPC框架原理

分布式计算的 CS 模式,总是由 Client 向 Server 发出一个执行若干过程请求,Server 接受请求,使用客户端提供的参数,计算完成之后将结果返回给客户端。

由于提供端与客户端分属于不同的进程乃至不同的机器,那么必定是需要通过网络进行通信,并且需要是可靠的网络。

通信框架一般是这样:

user->序列化->通信->反序列化->server

其实现需解决三个部分:

1.调用映射

比如调用者要调用RPC的login方法,怎么保证它不会调用到register方法呢?

这就需要RPC框架建立一个映射表,

比如{server_name,severinfo},serverinfo包含server对象

以及{method_name,methoddescriptor}

先解析server_name,得到要连接的server的ip和端口,再通过method_name找到要调用的函数

2.序列化与反序列化

一般的参数调用,系统会将参数放进内存中,但是远程调用又是另一台机器,不能通过内存传递函数,需要RPC客户将参数序列化为字节流,传给RPC提供方,它进行反序列化,得到参数。

这个序列化反序列化方法之一是Protobuf,它的字节流是用二进制存储的

并且Protobuf提供了很完备的RPC服务接口,很方便通过.proto文件生成服务器与客户端需要的RPCService类与Stub类。

3.网络传输

RPC既然是远程调用,离不开网络传输,可以使用muduo库

是一个linux多线程网络库,使用的是reactors in threads - one loop per thread模式,支持多线程,高并发。

来源: CSDN Jacky__Ren

使用最广泛的 Spring Cloud,基于 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。

1.服务集成RPC后,Provider启动后会通过Register模块,把服务的唯一ID和IP地址,端口信息等注册到RPC框架注册中心

2.调用者(Consumer)想要调用服务的时候,通过Provider注册时的服务唯一ID去注册中心查找在线可供调用的服务,返回一个IP列表(notify部分)

3.Consumer根据一定策略,比如随机或轮询从Register返回的可用IP列表真正调用服务(invoke)

4.最后是统计功能,RPC框架提供监控功能,监控服务监控状态,控制服务线上扩展和上下线

主流RPC

RMI(java自带的远程方法调用工具)

Hessian(基于HTTP的远程方法调用)

Dubbo(淘宝开源的基于TCP的RPC框架)

总结

工作原理

Provider:服务提供方,CS 模型中的 Server。
Consumer: 调用远程服务服务消费方,CS 模型中的 Client。
Registry:服务注册与发现的服务管理中心。
Monitor:统计服务的调用次数和调用时间的监控中心。
Container:服务运行容器,如 jetty。

执行过程

服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务,暴露自己的 IP 和端口信息。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者列表给消费者,如果有变更,注册中心将基于长连接推送给数据消费者。
服务消费者,从提供这地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另外一台服务调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时发送一次统计数据到监控中心。

来源:CSDN Q.E.D.

补充

很多时候不会使用using namespace std;(会不容易分辨各类函数来源)

以vector为例,自己的库定义了一个vector,标准库又有一个,这时候引用using namespace std;

会出现分歧

大型程序往往会使用多个独立开发的库,这些库会定义大量的全局名字,如类、函数和模板等,不可避免会出现某些名字相互冲突的情况。命名空间namespace分割了全局命名空间,其中每个命名空间是一个作用域。

1
2
3
namespace foo {
class Bar { /*...*/ };
} // 命名空间结束后无需分号

c++三大特性

多态:

主要指泛型编程运行时多态;在基类的函数加上virtual关键字,派生类重写该函数,运行时会根据对象的实际类型调用相应的函数。特指方法的多态

多态性,允许将子类类型的指针复制给父类类型的指针

重载实现编译时多态,虚函数实现运行时多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Dog.java

public class Dog{
public void shout(){
System.out.println("狗叫=狗");
}
//看门

}

Cat.java

public class Cat{
public void shout(){
System.out.println("猫叫=喵");
}
//抓老鼠

}

Boy.java

public class Boy{
public void play(Cat c){
c.shout();
}
public void play(Dog d){
d.shout();
}
}

主程序调用

Boy b=new Boy();
Cat c=new Cat();
Dog d=new Dog();
b.play(c);

猫叫=喵

那如果和动物玩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Animal.java

public class Animal{
public void shout(){
System.out.println("动物叫");
}
}

Cat.java

public class Cat extends Animal{
public void shout(){//重写
System.out.println("猫叫=喵");
}
//抓老鼠

}

Dog.java

public class Dog extends Animal{
public void shout(){//重写
System.out.println("狗叫=狗");
}
//看门

}

Boy.java

public class Boy{
public void play(Animal an){
an.shout();
}

}


主程序

Boy b=new Boy();

Animal an=new Animal();//动物
Dog d=new Dog();//具体的狗
an=d;//让动物是一只具体的狗

//合成一句Animal an=new Dog();
b.play(an);

狗叫=狗

多态三要素

继承、重写、父类引用指向子类对象

封装:

封装隐藏了实现细节,使得代码模块化

该隐藏的隐藏,该暴露的暴露

程序设计,追求”高内聚,低耦合”

高内聚:类的内部细节自己完成,不允许外部干涉

低耦合:仅对外暴露少量的方法用于使用

隐藏对象内部的复杂性,只对外公布简单的接口,便于外界调用。

我以java为例

Person.java中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Person{
private String name;
private int age;

//显式编写空构造器
public Person(){
System.out.println("空构造器");
age=19;
name="yuleiyun";
}

//如果参数与属性重名,会发生就近原则,所以对于属性,加this.
public Person(String name,int age){
this.name=name;
this.age=age;
}

//给一个赋值方法

//简单的封装思想
public void fuzhi(String name,int age){
this.name=name;
if(age>30) this.age=18;
}
//给一个读取方法
public String duquname(){
return name;
}

public int duquage(){
return age;
}
}

主函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HelloWorld {
public static void main(String []args) {

Person p1=new Person();
//如果Person类的属性是私有,那就无权限访问了,需要设定方法去访问私有属性
//System.out.println(p1.name+","+p1.age);

//访问属性时,调用方法即可
System.out.println(p1.duquname());
System.out.println(p1.duquage());


Person p2=new Person();
// System.out.println(p2.name+","+p2.age);
p2.fuzhi("YIYIY",123);
//用户调用fuzhi时,年龄大的,会给它美化,但用户不清楚细节
System.out.println(p2.duquname());
System.out.println(p2.duquage());
}
}

空构造器
yuleiyun
19
空构造器
YIYIY
18

上面代码的封装体现:

  • 属性私有化了
  • 但是提供public方法,供别人访问
  • 但别人也不能随便访问,程序员可以在方法中加入限制(比如将大于30的年龄,转化为了18)

继承:

子类继承父类的数据和方法,提高代码复用

比如,人这个基类,属性:姓名,年龄,身高;方法:吃饭、睡觉、说话

那派生类老师,属性:教师编号;方法:教书

那派生类学生,属性:学生编号;方法:学习

比如Person.java基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Person{
private String name;
private int age;
private double height;

//赋值与读取属性
public String getname(){
return name;
}
public void setname(String name){
this.name=name;
}

public int getage(){
return age;
}
public void setage(int age){
this.age=age;
}

public double getheight(){
return height;
}
public void setheight(double height){
this.height=height;
}

//吃饭
public void eat(){
System.out.println("人吃饭");
}
//睡觉
public void sleep(){
System.out.println("人睡觉");
}
//说话
public void talk(){
System.out.println("人说话");
}

}

派生类Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student extends Person{
//扩展子类的属性即可
private int sno;
//扩展子类的方法即可
public int getsno(){
return sno;
}
public void setsno(int sno){
this.sno=sno;
}
//学习
public void study(){
System.out.println("学生可学习");
}
}

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test{
public static void main(String []args) {
//定义一个子类具体的对象:
Student s=new Student();
s.setsno(1001);
s.setage(18);
s.setname("yuleiyun");
s.setheight(170);

s.study();
s.sleep();
s.eat();
s.talk();
}
}

学生可学习
人睡觉
人吃饭
人说话

泛型编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
T max1(T a, T b)
{
return a>b?a:b;
}
//#endif
int main(){

cout<<max1(2,3)<<endl;
cout<<max1(3.1f,2.6f)<<endl;
cout<<max1('a','b')<<endl;
cout<<max1("abc","abb")<<endl;
return 0;
}

3
3.1
b
abc

重载与重写的区别

重载:

是一个类中多态性的体现, 同一个类不同函数使用相同函数名,但函数的参数个数类型不同,有不同的返回类型

1
2
3
4
5
6
7
Person.java中
public Person(){

}
public Person(String name,int age){
System.out.println(name+age);
}

重写:

子类对父类函数的重新实现函数名和参数与父类一样,子类与父类函数体内容不一样,子类返回类型必须与父类保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Person.java
...
//吃饭
public void eat(){
System.out.println("人吃饭");
}
...
派生类 Student.java
...
//主要改函数体的内容
public void eat(){
System.out.println("我要吃汉堡嘞!");
}
...

主程序调用
Student s=new Student();
s.eat();

我要吃汉堡嘞!

指针和引用区别

&叫做地址运算符,*是间接运算符

int* ptr;//这样的书写方式说明ptr数据类型不是int,而是int指针

cout<<*ptr,这将解引用指针,实际上使用指针指向的值

1
2
int a=10;
cout<<a<<","<<&a<<","<<*&a;// 10,0x72fe0c,10
  • 指针是一个新的变量,存储的是地址,可通过访问这个地址来修改另一个变量;

    引用属于原变量的别名,对引用的任何操作就是对变量本身操作

  • 指针可以有多级,引用就一级

  • 指针可以为空,引用定义时必须初始化

  • 指针初始化后可以改变指向,但引用不可以

  • sizeof指针得到的是该指针的大小,sizeof引用是引用所指向变量的大小

  • 指针作为参数传递时(将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量),在函数中改变这个变量的指向不影响实参,引用却可以

    (即,指针传参还是值传递,指针本身的值不可改,除非解引用对指向的对象进行操作;

    引用传参,传进来的就是变量本身,因此变量可以修改

    )

  • 引用本身不是一种数据类型,不占用存储单元;指针是具体的变量,占用存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void testPTR(int *p)
{
int a=12;
p=&a;
}
void testREFF(int &p)
{
int a=12;
p=a;
}
int main()
{
int a=10;
int* b=&a;
testPTR(b);
cout<<a; //10
cout<<*b;//10
——————————————
a=10;
testREFF(a);
cout<<a;//12
}

引用变量

1
2
3
4
5
6
7
8
9
10
void ss(int &a)
{
a=7;
}
...
int b=5;
std::cout<<&b<<","<<b<<std::endl;
ss(b);
std::cout<<&b<<","<<b;

0x72fdec,5
0x72fdec,7

引用作为函数返回类型

int& jisuan(int x){}

  1. 避免复制: 使用引用作为返回类型可以避免在函数调用时进行数据的复制操作。当返回引用时,实际上是将调用方的变量和函数内部的对象绑定在一起,而不是创建一个新的副本。这可以提高程序的性能,尤其是当返回的对象很大时。
  2. 允许修改: 返回引用使调用方能够修改原始对象。这在需要从函数内部修改调用方的变量时非常有用,特别是在函数内部修改一个对象而不需要使用指针的情况下。
  3. 链式调用: 使用引用作为返回类型,可以实现链式调用。这在某些情况下可以使代码更加清晰和简洁。例如,很多标准库的操作就采用了链式调用的方式。
  4. 传递信息: 引用作为返回类型可以传递更多的信息,如返回一个布尔值来指示函数执行的成功与否。这种情况下,函数可以直接修改调用方提供的变量来传递额外的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 int  a=4;
2 int &f(int x)
3 {
4 a = a + x;
5 return a;
6 }
7 int main()
8 {
9 int t = 5;
10 cout<<f(t)<<endl; //a = 9
11 f(t) = 20; //a = 20
12 cout<<f(t)<<endl; //t = 5,a = 25
13 t = f(t); //a = 30 t = 30
14 cout<<f(t)<<endl; //t = 60
15 return 0;
16 }

结构体为什么要内存对齐

平台原因,有的平台只能在某些地址处取特定类型的数据,否则抛出硬件异常

硬件原因,内存对齐之后,cpu内存访问速度大大提升

1、 分配内存的顺序是按照声明的顺序。

2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍,则空出内存,直到偏移量是整数倍为止

3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍

添加了#pragma pack(n)后规则就变成了下面这样:

1、 偏移量要是n和当前变量大小中较小值的整数倍

2、 整体大小要是n和最大变量大小中较小值的整数倍

3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

1
2
3
4
5
6
7
8
9
10
11
12
struct Test{
int a;//4
char b;//1
int c;//4 总共会是9字节吗
}test;
int main()
{
test.a=1;
test.b=2;
test.c=3;
cout<<sizeof(test)<<endl;
}

其实是12字节

以0x0000为起始地址,32位操作系统讨论

0x01 0x00 0x00 0x00
变量a

还空了三字节

0x02
变量b
0x03 0x00 0x00 0x00
变量c

对于变量b,c,如果无内存对齐情况,这是5字节

前4字节

0x02 0x03 0x00 0x00

后1字节

0x00

这样的话,要读取两次,再拼接;而内存对齐,一次读取即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
double a;//4
char b;//1
double d;//8 总共的sizeof是24(8+8+8)

int a;//4
char b;//1
int c;//4
double d;//8 //总共是24 (4+4+4+8)=20


cout<<offsetof(Test,a)<<endl;//0
cout<<offsetof(Test,b)<<endl;//4
cout<<offsetof(Test,c)<<endl;//8
cout<<offsetof(Test,d)<<endl;//16


int a;//4
char b;//1
double d;//8 //总共是16 (4+4+8)

综上,所以类型大小小的那个,本身所占的空间取决于其上一个类型

int a;//4
double d;//8
char b;//1 //总共是24 那就是说本来是4+8+8为20,因为要满足最大类型的整数倍,那就24咯?


cout<<offsetof(Test,a)<<endl;//0
cout<<offsetof(Test,d)<<endl;//8
cout<<offsetof(Test,b)<<endl;//16

传递函数参数,什么时候用指针、引用

指针:

  • 返回函数内局部变量的内存时。使用指针传参需要开辟内存,用完释放,否则引起内存泄露。而返回局部变量的引用无意义

引用

  • 对栈空间大小比较敏感(比如递归)时用引用,引用传递不需要创建临时变量,开销更小
  • 类对象作为参数传递要用引用

堆和栈区别

申请方式

  • 栈由系统自动分配
  • 堆是自己申请、释放

申请大小限制

栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定

堆向高地址扩展,是不连续的内存区域,大小灵活调整(这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,链表的遍历方向是由低地址向高地址)

申请效率

栈由系统分配,速度快,无碎片 (先进后出,进出一一对应)

堆有程序员分配,速度慢,有碎片

分配方式和空间大小

堆,动态分配,不连续

栈,动态、静态都有,连续内存区域

栈空间默认4M,堆区一般1G-4G

栈就像点菜,申请、使用,快捷,但自由度小

堆,自己做菜,麻烦,自由度大

宏定义和函数区别

1.执行速度

宏在编译时完成替换,被替换的文本参与编译,相当于直接插入了代码;运行时不存在函数调用,执行更快;

而调用函数,需要跳转到具体调用函数

2.返回值

宏定义相当于在结构中插入代码,无返回值;

函数调用是有的

3.类型检查

宏定义无类型,不进行类型检查

函数要

4.宏定义,不是语句,最后不加分号

1.预处理阶段:当编译c++代码时,预处理器过一遍c++所以#开头的语句,预编译器将这些代码评估完后给到编译器进行实际的编译

2.用宏的目的:将代码中的文本替换为其它东西

1
2
3
4
5
6
7
8
#define WAIT std::cin.get()
//可以不放分号,否则会加入宏了
int main()
{
WAIT;
//等价于std::cin.get()
//当然,这种操作很蠢,只能让自己读懂代码
}

3.用法1:发送参数

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>

#define log(x) std::cout<<x<<std::endl
using namespace std;

int main(){

log("hello");//输出hello
return 0;
}

用法2:辅助调试

debug模式下会有很多日志的输出,Release模式下就不需要日志的输出了,正常的方法会删除 很多输出日志的语句或函数。 宏可以直接取消掉这样的语句

利用#if,#else,endif实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>

#define PR_DEBUG 1
#if PR_DEBUG==1
#define log(x) std::cout<<x<<std::endl
#else
#define log(x) //什么也不定义 无意义
#endif
using namespace std;

int main(){

log("hello");//当PR_DEBUG 为 1,输出,为0,无输出
return 0;
}

利用#if 0#endif删除一段宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>

#if 0

#define PR_DEBUG 1
#if PR_DEBUG==1
#define log(x) std::cout<<x<<std::endl
#else
#define log(x) //什么也不定义 无意义
#endif
using namespace std;

#endif
int main(){

log("hello");
return 0;
}
//输出 error: 'log' was not declared in this scope

宏和模板的区别

发生时间不同,宏在预处理阶段就被评估了

宏和typedef区别

1.用途

宏主要用于定义常量和书写复杂的内容

typede定义类型f别名

2.时间

宏替换发生在编译阶段之前,文本插入替换

typedef是编译的一部分

3.宏不检查类型,typedef检查数据类型

4.宏不是语句,末尾不加分号;typedef要

5.指针的操作不同 ,typedef char * p_char和#define p_char char *区别巨大。

变量声明和定义区别?

  • 声明仅仅把变量的声明的位置及类型提供给编译器,不分配内存空间

定义,要在定义的地方为其分配存储空间

  • 相同变量多处声明(外部变量extern) ,但只能在一处定义
1
2
3
4
5
//变量声明
extern int x;
//变量定义
int x;
int y=10;

总之,变量声明通知编译器变量的存在和类型,而变量定义创建变量,为其分配内存,

总结:

声明就是告知一下,你知道有这么个变量或者函数,这个变量的具体值,还有这个函数的函数体,不用知道;就告诉基本信息需要extern关键字

定义就是,变量的初始化啊,函数体的具体实现

如果是在类中的话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {
public:
Person(); // 成员函数的声明
void SetName(const std::string& name); // 成员函数的声明
void SetAge(int age); // 成员函数的声明
void PrintInfo() const; // 成员函数的声明

private:
std::string name_; // 成员变量的声明
int age_; // 成员变量的声明
};


Person::Person() : age_(0) {
// 构造函数的定义
}

void Person::SetName(const std::string& name) {
name_ = name;
}

void Person::SetAge(int age) {
age_ = age;
}

void Person::PrintInfo() const {
std::cout << "Name: " << name_ << ", Age: " << age_ << std::endl;
}

strlen和sizeof区别

sizeof运算符,结果在编译时得到,所以不能得到运行时分配的存储空间大小;strlen是库函数

sizeof参数可以是任何数据的类型或数据;strlen的参数只能是字符指针且结尾是’\0’的字符串

a与&a的区别

假设数组int a[10] 与int(*p)[10]=&a

a是数组名,是数组首元素地址,*(a+1)=a[1],a相当于&a[0]

&a是数组的指针,其类型为int(*p)[10],值就是地址了,如果+1,其实是整个数组的偏移,地址为尾元素的后一个元素地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){

int a[4]={1,2,3,4};
int (*p)[4]=&a;

std::cout<<a<<","<<&a<<std::endl;//0x72fdf0,0x72fdf0

std::cout<<*a<<","<<*&a[0]<<std::endl;//1,1

std::cout<<*(a+1)<<","<<*(&a[1])<<std::endl;//2,2
//也就是说(&a+1),其实是整个数组的偏移,地址为尾元素的后一个元素地址
std::cout<<(a+4)<<","<<(&a+1)<<std::endl;//0x72fe00,0x72fe00

std::cout<<*(a+1)<<","<<*(*p+1)<<std::endl;//2,2
return 0;
}

c++与python

python是解释执行的脚本语言;c++是在特定平台运行的编译语言

python缩进区分代码块;c++花括号

python库多,调用方便,不需要事先定义变量类型,基本数据类型为数字、布尔值、字符串、列表、元组等等

c++与c

c没有字符串类型

c++的new取代了c中的malloc

c++的控制态输入输出iostream类替代c的stdio函数

c++允许重载,c不允许

c++允许重复定义变量,变量定义语句在使用它之前都可以;但c不能重复定义,且必须在函数开头部分

c++多了关键字 bool、using、namespace等等

c的特点

面向过程的结构化语言,易于调试和维护

表现能力和处理能力极强,可以直接访问内存的物理地址

c实现了对硬件的编程操作,也适合应用软件的开发

c++特点

在c的基础上扩充和完善,使c++兼容了c语言的面向过程特点,又成为了一种面向对象的程序设计语言

可以使用抽象数据类型进行基于对象的编程

可以使用多继承、多态进行面向对象的编程

担负起以模板为特征的泛型化编程

c++与java

语言特性

java完全面向对象,jvm(java虚拟机)安装到任何操作系统上,可移植性强

java没有指针的概念

java用接口取代了c++中的抽象类,省却了在实现和维护上的复杂性

垃圾回收

c++析构函数回收

java是内存的分配和回收都是自动的

应用场景

java在web应用上相比具有优势 ,有丰富的框架

底层程序的编程以及控制方面的编程,c++灵活,有句柄的存在

java在桌面程序的实用性不如c++;c++可直接编译成exe文件,指针是c++的优势,可以直接对内存操作,但有危险性

c++的class和struct

相同

都有成员函数、公有、私有部分

任何可以用class完成的工作,可以用struct完成

不同点

如果不对成员不指定公私有,struct默认公有。class默认私有

class默认private继承,struct默认public继承

define与const

编译阶段

define编译的预处理阶段起作用

const在编译、运行时起作用

安全性

define只做替换,不做类型检查和计算,不求解

const常量有数据类型,编译器对其进行类型安全检查

内存占用

define只是将宏名称进行替换,内存中有多个相同的备份;const就一份

宏定义的数据没有分配内存空间,const定义的变量只是值不能改变,但要分配内存空间

const和static的作用

static

不考虑类的情况

1.隐藏

不加static的全局变量和函数具有全局可见性,可在其他文件中使用,加了只能在该文件所在的编译模块中使用

2.默认初始化为0

包括未初始化的全局静态变量与局部静态变量

3.静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性

作用范围与局部变量相同,函数退出后仍然存在,但不能使用

应用场景:两个全局变量的名字不能一样

1
2
3
4
5
6
7
8
//在a.cpp中
//int s_var=5;//冲突了
static int s_var=5;
//main.cpp中
int s_var=10;
//或者extern int s_var;
...
cout<<s_var;// 10

考虑类

static成员变量:

只与类关联,不与类的对象关联,定义时要分配空间,不能在类声明中初始化

必须在类定义体外部初始化(为了分配内存),初始化时不需要标识为static,可以被非static成员函数任意访问

static成员函数:

不具有this指针(本质上,写的所有非static方法都会获得当前类实例作为参数)

没有类实例

无法访问类对象的非static成员变量和非static成员函数;

不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

struct Entity
{
static int x;

void print()
{
cout << x << endl;
}
};

int Entity::x;

int main()
{
Entity e1;
e1.x = 1;

Entity e2;
e2.x = 2;

e1.print();//2
e2.print();//2

cin.get();
}

两个实例化对象共享的是同一个变量,正因如此,通过类实例来引用静态变量是没有意义的

所以可以这样写:

1
2
3
4
5
6
7
8
Entity e1;
Entity::x = 1;

Entity e2;
Entity::x = 2;

e1.print();//2
e2.print();//2

如果连print()也是静态的话,由于静态函数无实例,可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Entity
{
static int x;

static void print()
{
cout << x << endl;
}
};

int Entity::x;
int main()
{
Entity::x = 1;
Entity::x= 2;
Entity::print();//2
Entity::print();//2

cin.get();
}

当然了,此时将x改为非static,会报错,因为static函数无法访问非静态变量(静态方法无实例)

所以,静态方法和在类外部编写的方法一样

如果在类外面写一个print(),访问不到x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Entity
{
int x;

static void print()
{
cout << x << endl; // 报错,不能访问到非静态变量x
}
};
//在类外面写一个print()函数
static void print()
{
cout << x << endl; // 报错,x是什么?没被定义。
}

int main()
{
Entity e1;
e1.x = 1;

Entity e2;
e2.x = 2;

e1.print();
e2.print();

cin.get();
}

下面这种就可以

1
2
3
4
5
//在类外面写一个print()函数
static void print(Entity e)
{
cout << e.x << endl; // 成功运行
}

const

  • const首先作用于左边的东西;如果左边没东西,就做用于右边的东西
  • const被cherno称为伪关键字,因为它在改变生成代码方面做不了什么。
  • const是一个承诺,承诺一些东西是不变的,你是否遵守诺言取决于你自己。我们要保持const是因为这个承诺实际上可以简化很多代码。

不考虑类

const常量在定义时 必须初始化,之后无法更改

const形参可以接收const和非const类型的实参

考虑类

const成员变量:

不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;

不同类对其const数据成员的值可以不同,所以不能在类中初始化

const成员函数:

const对象不可以调用非const成员函数;

非const对象都可以调用

不可以改变非mutable数据的值

mutable:该关键字声明的变量可以在const成员函数中被修改

顶层const和底层const

顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是*号的右边

底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是*号的左边

1
2
3
4
5
int a=10;int* const b1=&a;//顶层const,b1本身是个常量
const int* b2=&a;//底层const,b2本身可变,所指对象是常量
const int b3=20;//顶层const,b3常量不可变
const int* const b4=&a;//前一个const为底层;后一个为顶层,b4不可变
const int& b5=a;//用于声明引用变量,底层const

区分作用:

执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const

使用命令强制类型转换函数const_cast时,只能改变运算对象的底层const

1
2
3
const int a;int const a;//定义常量类型a
const int* a;//a为指向int型变量的指针,const在*左侧,表示a指向不可变常量
int *const a;//a指向int型变量,const在*右侧,表示常指针,即该指针初始化后,不可更改指向

数组名与指针的区别

都可以通过增减偏移量来访问数组中的元素

数组名理解为常指针,所以数组名没有自增自减操作

数组名做形参传递给调用函数后,失去原有特性,退化为一般指针,可以自增、自减,但sizeof运算符不能得到原数组大小

1
2
3
4
5
6
7
8
9
10
11
12
1 char a[] = "hello world";
2 char *p = a;
3
4 //计算数组和指针的内存容量
5 cout<< sizeof(a) << endl; // 12 字节
6 cout<< sizeof(p) << endl; // 4 字节
7
8 //数组作为函数参数传递
9 void Func(char a[100])//数组退化成指针
10 {
11 cout<< sizeof(a) << endl; // 4 字节而不是100 字节
12 }

final和override关键字

override

父类中用了虚函数,子类对父类的虚函数进行重写

1
2
3
4
5
6
7
8
class A
{
virtual void foo()=0;
};
class B:public A{
//void foo(){}也是可以的
void foo() override{};//加了override,如果函数名写错了,编译不通过
};

final

不希望某个类被继承 or 不希望某个虚函数被重写;可以添加该关键字,一旦添加该关键字的类被继承,或方法被重写,编译报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base{
virtual void foo()=0;
};

class A:public Base
{
void foo() final;
};
class B final:A{
void fool(); //error,A里已经final了
};

class C:public B{//error,B类final

};

拷贝初始化与直接初始化

用于类类型对象时:

直接初始化:

直接调用与实参匹配的构造函数

拷贝初始化:

先使用指定构造函数创建一个临时对象,调用拷贝构造函数将临时对象拷贝到正在创建的对象

1
2
3
4
5
6
7
//直接初始化
string str1("i am a string");
string str2(str1);

//拷贝初始化
string str3="I am a string";
string str4=str3;//相当于隐身调用拷贝构造函数,而不是复制运算符函数

当然了,拷贝构造函数为private时,语句3,4编译时会报错

explict修饰构造函数时:如果构造函数存在隐式转化,也报错

什么时候调用拷贝构造函数

用类的一个实例化对象去初始化另一个对象的时候

函数的参数是类的对象时(非引用传递)

函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数

编译型与解释型语言区别

执行方面

编译型语言,先用编译器将代码编译成可执行文件如.exe才能执行

即源程序->机器指令->机器运行

解释型,解释(翻译)一条执行一条

跨平台性

编译型依赖特定平台,不同操作系统下使用不同编译工具进行编译,编译出来的机器代码不同

解释型移植性好(jvm(java虚拟机)、python虚拟机的强大)

性能

编译型要好一点

解释型语言中间隔了个虚拟机的存在,运行速度慢了,要求的内存也比较高

总之,编译型,运行快,所需内存小,但跨平台性差

​ 解释型,跨平台性好,但运行慢,内存大

初始化和赋值的区别

初始化:创建对象时赋予一个初值,分为直接初始化、拷贝初始化

赋值:将对象的当前值擦掉,用新值代替

简单类型来说没区别

对于类和复杂类型的话

怎么区分嘞:对象创建的时候就是初始化,未创建对象而对象的值改变就是赋值

extern “C”的用法

为了在c++代码中正确调用c语言代码,在程序中加上 extern “C”,相当于按照C语言进行编译了

使用场景:你比如多人协同开发,有人c++,有人c

使用例子:https://blog.csdn.net/m0_58367586/article/details/123015442

野指针与悬空指针

都是指向无效内存区域的

野指针:

野指针,指的是没有被初始化过的指针

比如int *p;为了防止出错,可这样 int* p=nullptr;

解决:要么初始化、要么置空

悬空指针:

最初指向的内存已经被释放了的指针,如下p和p2都是悬空指针

解决:释放操作后,及时置空

1
2
3
4
5
6
7
...
int* p=nullptr;
int* p2=new int;

p=p2;

delete p2;

所以,c++引入了智能指针,避免悬空指针的产生

C++类型安全

什么是类型安全?

类型安全很大程度上等价于内存安全,类型安全的代码不会试图访问自己没被授权的区域。

c++的类型安全

操作符new返回的指针类型严格与对象匹配,而不是void*

dynamic_cast比static_cast有更多具体的类型检查

const关键字(有类型、作用域)代替#define constants(简单的文本替换)

c++的构造函数

默认构造函数(无参)

1
2
3
4
5
6
7
8
9
class Student
{
...
student(){
this->id=1;
this->name="yuleiyun";
}
...
};

初始化构造函数(有参数)

1
student(int a, string str):id(a), name(str){};

拷贝构造函数

1
2
3
4
student(const student& s){
this->id = s.id;
this->name = s.name;
};

移动构造函数(move和右值引用)

委托构造函数

转换构造函数(形参只有一个,是其他类型变量)

1
2
3
4
student(int id){ //将其它类型变量,隐式转换为本类对象
this->id = id;
this->name = "yuleiyun";
};

浅拷贝与深拷贝(暂略)

当进行变量被赋值给另一变量的时候,这是在进行复制

浅拷贝只是拷贝基本数据类型(非指针变量),并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址

如果对象中变量带有指针或引用,再释放浅拷贝的指针资源会出错

深拷贝不仅拷贝指针,还开辟出一块新的空间用来存放新的值。不会出现浅拷贝出错的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using namespace std;
class Person{
private:
int num;
char* name;
public:
Person(){
name=new char(20);
cout<<"Person"<<endl;
};
~Person(){
cout<<"~Person "<<&name<<endl;
delete name;
name=NULL;
};
Person(const Person &p){
//浅拷贝
name=p.name;

//深拷贝
// name=new char(20);
// memcpy(name,p.name,strlen(p.name));
cout<<"Copy Person"<<endl;
};
void getname()
{
cout<<name<<endl;
}
};
int main(){
{
Person p1;
Person p2(p1);//复制
// p2.getname();
}

system("pause");
return 0;
}

但是我的浅拷贝没报错

内联函数与宏定义

在使用时,宏只做简单字符串替换(编译前);

而内联函数可以进行参数类型检查(编译时),且具有返回值

内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载

宏定义时要注意书写(参数要括起来)否则容易出现歧义;

内联函数不会产生歧义,内联函数有类型检测、语法判断等功能,而宏没有

内联函数使用场景

使用宏定义的地方都可以inline函数

作为类成员接口函数来读写类的私有成员或保护成员,提高效率

大小端存储

大端:字数据的高字节存储在低地址

小端:字数据的低字节存储在低地址

在socket编程中,往往将操作系统使用的小端存储的IP地址转化为大端存储,从而进行网络传输

例如32字节的0x12345678(高字节->低字节)

小端存储方式:

内存地址 0x2000 0x2001 0x2002 0x2003
内存内容 0x78 0x56 0x34 0x12

大端存储方式:

内存地址 0x2000 0x2001 0x2002 0x2003
内存内容 0x12 0x34 0x56 0x78

代码判断

1.强制类型转换

1
2
3
4
5
6
int a=0x1234;//int 与 char的长度不同,int转换成char,只会留下低字节部分
char c=(char)(a);
if(c==0x12)
cout<<"大端"<<endl;
else if(c==0x34)
cout<<"小端"<<endl;

2.巧用union联合体

volatile、mutable和explicit关键字的用法

volatile

volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。

多线程下:有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。

mutable

mutable:可变的、易变的,在C++中,mutable 也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const 函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。 但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被 mutable来修饰,并且放在函数后后面关键字位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A=10;//错误 不可修改值,this已经被修饰为常量指针
m_B=20;//正确
}
}
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
}
int main()
{
const person p;//修饰常对象 不可修改类成员的值
p.m_A=10;//错误,被修饰了指针常量
p.m_B=200;//正确,特殊变量,修饰了mutable
}

explicit

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换,注意以下几点:

explicit 关键字只能用于类内部的构造函数声明上

explicit 关键字作用于单个参数的构造函数

被explicit修饰的构造函数的类,不能发生相应的隐式类型转换

c++的异常处理方法

常见异常:

数组下标越界

除数为0

动态分配空间不足

1.try、throw、catch关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>

using namespace std;

int main(){
double m,n;
cin>>m>>n;
try{
cout<<"before dividing: "<<endl;
if(n==0) throw -1;//抛出int型异常
else if(m==0) throw -1.0;//抛出double型异常
else cout<<m/n<<endl;

cout<<"after dividing. "<<endl;
}
catch(double d){
cout<<"catch (double) "<<d<<endl;
}
catch(int c){
cout<<"catch (int) "<<c<<endl;
}
cout<<"finished. "<<endl;
return 0;
}

输入m=1,n=0

before dividing:
catch (int) -1
finished.

输入m=0,n=1

before dividing:
catch (double) -1
finished.

输入 m=4,n=2;

before dividing:
2
after dividing.
finished.

程序的执行流程是:

先执行try包裹的语句块:

​ 如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块

如果发生异常, 则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息

代码中使用的是数字,也可以自定义异常class

catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换), 如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)

当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。

2.函数的异常声明列表

int fun() throw(int,double,A,B,C){...};

3.c++标准异常类exception

bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋 出此异常

bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如 果转换是不安全的,则会拋出此异常

bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

形参、实参区别

  • 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元
  • 实参是一个确定的值(常量、函数、变量、表达式),获取输入也好、预先赋值也好使实参获得确认值,产生临时变量
  • 它们在个数、类型上、顺序上严格匹配,否则调用函数出错
  • 函数调用中数据单向传递,实参->形参;所以形参的值变化,不影响实参
  • 当形参、实参不是指针类型:
    • 在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置
    • 形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变

值传递、指针传递、引用传递的区别和效率

值传递

形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象,耗费一定的时间和空间

指针传递

传地址值,同样有一个形参向函数所属栈拷贝数据的过程,但拷贝数据固定为4字节的地址

引用传递

同样有上述拷贝过程,但是针对地址的,为该数据所在地址起了一个别名

静态变量什么时候初始化

const关键字作用

  • 防止变量改变,防止指针更改指向
  • 修饰形参,在函数内部不能修改
  • 对于类的成员函数,const修饰表示常函数,不能修改类的成员变量;类的常对象只能访问类的常成员函数
  • 对于类的成员函数,必须指定其返回值为const类型,使得其返回值不为左值
  • const成员函数可以访问 非const对象的非const或const数据成员、const对象内的所有数据成员
  • 非const成员函数不能访问const对象的任意数据成员
  • 引用或指针传递函数调用时,因为函数内部可以改变引用或指针所指向的变量,用const修饰形参才是实在地保护了实参所指向变量

类的继承

子类或派生类

特点为:

子类拥有父类所有属性和方法

子类可以拥有父类没有的属性和方法

子类对象可以当做父类对象使用

c++编译过程

预处理->编译->汇编->链接

预处理

预处理,对伪指令(以#开头的指令,如:宏定义、条件编译(#if #endif)、头文件)和特殊符号(注释、三元符)处理

预处理指令包括(#include、#define、#pragma)

编译

编译器,对上步的文件词法分析、语法分析、语义分析,生成汇编代码文件

汇编

汇编器,将汇编代码转化为机器可执行的二进制代码(机器码),并生成目标文件

链接

链接器,解决多个文件之间符号引用问题;

因为编译时,编译器只对单个文件进行处理,如果该文件需要引用到其他文件中的符号的话,比如全局变量或调用某个库函数中的函数,需要由链接器把所有的目标文件链接到一起才能确定最终地址,生成最终可执行文件

c++内存的5个区

1、栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数。(为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区)。

栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

2、堆:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。

3、 代码区:存放程序的二进制代码

4、全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。(全局变量、静态数据、常量存放在全局数据区),使用静态关键字static声明,在静态存储区申请一个静态变量

5、 常量存储区:存储常量,不允许被修改。

1
int* p=new int[5];

看到new,说明分配了一块堆内存;而指针p嘞,分配的是一块栈内存;

该语句是:在栈内存中存放了一个指向一块堆内存的指针p,调用operator new 分配内存,然后返回这块内存的首地址,放入栈中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 0; //全局初始化区
char *p1; //全局未初始化区
int main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

c++没有垃圾回收机制

首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计 数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。 垃圾回收会使得C++不适合进行很多底层的操作。

malloc、realloc、calloc

  1. malloc函数

    1
    2
    3
    void* malloc(unsigned int num_size);
    int *p = malloc(20*sizeof(int));申请20个int类型的空间;

  2. calloc函数

    1
    2
    void* calloc(size_t n,size_t size);
    int *p = calloc(20, sizeof(int));

    省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

  3. realloc函数

    1
    void realloc(void *p, size_t new_size)

    给动态分配的空间分配额外的空间,用于扩充容量。

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表 会快一些?

  1. 赋值初始化,通过在函数体内进行赋值初始化;

  2. 列表初始化,在冒号后使用初始化列表进行初始化。

    这两种方式的主要区别在于:

    对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的;

    相当于一次默认构造+一次赋值

    列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

    相当于一次赋值操作

    其相比赋值初始化而言,少了调用构造函数的过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class test{
    private:
    string val;
    test* tep;

    public:
    test()//赋值初始化
    {
    this.val="yuleiyun";
    this->tep=nullptr;
    }

    test(string s):val(s),tep(nullptr){}//列表初始化
    }

    一个派生类构造函数的执行顺序如下:

    ① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)

    ② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)

    ③ 类类型的成员对象的构造函数(按照初始化顺序)

    ④ 派生类自己的构造函数。

方法一是在构造函数当中做赋值的操作,

而方法二是做纯粹的初始化操作

我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

string与c的char*

string是对char*的封装,string包含了char*数组的属性

string可以进行动态扩展

*内存泄露

一般指堆的内存泄露,使用malloc、new分配完内存,使用完后,程序必须调用相应的free或delete释放该内存块,否则,这块内存就不能被访问,从而造成系统内存资源的浪费

避免内存泄露:

1.计数法:使用new或malloc时,该计数++,delete或free时,该计数–,程序执行完打印这个计数,不为0则存在内存泄露

2.将基类的析构函数声明为虚函数

3.保证new/delete、malloc/free的成对出现

4.智能指针自动化地管理动态分配的内存资源

强制类型转换

reinterpret_cast

reinterpret_cast (expression):type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转 换。

const_cast

const_cast (expression) 该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。 一般修改底指针

static_cast

static_cast < type-id > (expression) 该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。

用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。

dynamic_cast

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast (expression) 该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*

c++函数调用的压栈过程

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/ 指针传递。

浮点数比较

浮点数的精度是有限,通过相减取绝对值,并与预设的精度比较

类的对象建立

main函数中

1
2
Test a;//静态建立,那就是存在栈上,销毁什么的自动
Test *b=new Test();//因为new,那就是在堆上,new Test()返回是Test对象的指针,销毁需要手动delete

组合与继承

继承

优点:子类重写父类方法实现扩展

缺点:

父类的内部细节对子类来说可见

是高耦合关系,如果父类的方法有修改,比如参数个数,子类必须做出相应调整

组合

将A类的对象作为B类的成员变量

优点:

所包含的对象的内部细节不可见

低耦合,修改了包含对象的代码,不需要修改当前对象类的代码

缺点:

容易产生过多的对象

必须对接口仔细定义

函数指针

意义:希望在同一个函数中通过使用相同的形参不同的时间使用产生不同的效果

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

1
int (*pf)(const int&, const int&); 

上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的

否则就是

1
int *pf(const int&, const int&);

而这声明了一个函数pf,其**返回类型为int ***, 带有两个const int&参数。

结构体变量比较相等

1
2
3
4
5
6
7
8
9
struct foo {
int a;
int b;
bool operator==(const foo& rhs) *//* *操作运算符重载*
{
return( a == rhs.a) && (b == rhs.b);
}
};

c++访问权限和继承权限

① public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;

② private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;

③ protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

三种继承方式

① 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权 限,在派生类中仍然保持原来的访问权限;

② 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;

③ 若继承方式是protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected) 权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

strcpy和memcpy的区别是什么

1、复制的内容不同。

strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。

2、复制的方法不同。

strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。

3、用途不同。

通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

*空类,默认添加哪些函数

1
2
3
4
1) Empty(); // 缺省构造函数//
2) Empty( const Empty& ); // 拷贝构造函数//
3) ~Empty(); // 析构函数//
4) Empty& operator=( const Empty& ); // 赋值运算符//

静态绑定与动态绑定

对象的静态类型:对象在声明时采用的类型,编译期确定

对象的动态类型:目前所指对象的类型。运行期觉得,对象的动态类型可以更改,静态类型无法更改

静态绑定:绑定对象静态类型

动态绑定:

C++中哪些运算符不能重载?

1 .(成员访问运算符)
2 .*(成员指针访问运算符)
3 ::(域运算符)
4 sizeof关键字
5 ?:(条件运算符)

c++编译模块是什么

什么是全局静态变量和局部静态变量

c++线程池

技术面(gg)

1
2
3
4
5
char a[] = "hello world";
char *p = a;
cout << sizeof(a) << endl;
cout << sizeof(p) << endl;
return 0;

char a[]相当于string,所以,字符串长度+”\0”的长度为11+1=12

第二个sizeof是指针的大小,一般为8字节

1
2
3
4
5
6
执行以下语句会有什么结果
char a[] = “hello”;
a[0] = ‘X’;

char *p = “world”;
p[0] = ‘X’;

前两行没问题

后两行,字符串常量通常被存储在只读的内存区域,不能修改

后两行运行时,会给出警告,但p[1],p[0]是可以访问的

1,4,7,11,15
2,5,8,12,19
3,6,9,16,22
10,13,14,17,24
18,21,23,26,30

有个MN矩阵,每一行每一列升序,用最优解找数字

思想:要么左下角,要么右上角开始找

下面代码以右上角开始:

如果a[row][col]>t呢,说明在左边col–

否则呢,说明在下边row++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
bool jisuan(vector<vector<int> > a, int t)
{
// for (auto we : a)
// for (auto we1 : we)
// {
// if (we1 == t)
// return true;
// }
// return false;
int row=a.size(),col=a[0].size();
int minv=min(row,col);

row=0;col=col-1;
while (row <a.size() && col >= 0)
{
if(a[row][col]==t) return true;
else if(a[row][col]>t) col--;
else row++;
}
return false;
}
int main()
{
vector<vector<int> > a;

string s;
while (1)
{
getline(cin, s);
if(s.empty()) break;
stringstream ss(s);
string t;
vector<int> b;
while (getline(ss, t, ','))
{

b.push_back(stoi(t));
}
a.push_back(b);
// if (s.size() == 0)
// if(s.empty()) //如果判断空行放在这里,到时候输入的t是不存在的值时,不会输出false;
// break;
}
int t;
cin >> t;


bool flag=jisuan(a, t);
if (flag)
{
cout << "true";
}
else
{
cout << "false";
}

return 0;
}

1,4,7,11,15
2,5,8,12,19
3,6,9,16,22
10,13,14,17,24
18,21,23,26,30

28

输出false

总结

任何东西都不是速成的,知识需要时间去检验,空有理论,没有实际应用,白搭

技术二面(gg)

友元

关键字friend

CSDN

一个空类会自动初始化多少函数

除了构造和析构

还有拷贝构造函数

复制运算符

赋值初始化和拷贝初始化(记不清了)

基类的析构函数设置为虚函数有什么用

释放基类的析构函数同时,释放派生类的析构函数

栈和队列区别

栈先进后出;队列先进先出

栈表的一端插入和删除;队列一端进一端出

栈只能取顶部元素,遍历数据的同时要为数据开辟临时空间;队列无需开辟空间,遍历速度稍快

*能重载吗

不可以

操作符重载用过吗

1
2
3
bool operator()(pair<int,int> &a, pair<int,int>&b){
return a.second < b.second;
}

static在类里,在类外,有什么区别

类里的static存在哪里,存在数据段

析构函数如果,会发生什么(记不清了)

崩溃

C++为什么面向对象(具体说明一下)

我答的是多态、封装和继承,这是特点

面向过程:主要是指功能的行为,以函数为单位,怎么做

把大象关进冰箱

1.开箱

2.把大象关进去

3.关箱

面向对象:主要是指具备了功能的对象,考虑谁去做

三个主体:人、冰箱、大象

冰箱{

​ open(){}

​ close(){}

};

大象{

​ 进冰箱(){}

};

人{

​ 打开(冰箱){

​ 冰箱.open();

}

​ 送(大象){

​ 大象.进冰箱();

}

关闭(冰箱){

​ 冰箱.close():

}

};

继承是什么关系

子类与父类之间的关系是“是一个”(is-a)关系,即子类是父类的一种特殊类型

嵌套和继承的区别

嵌套类是一个在另一个类的内部定义的类。它在外部类中具有作用域限制,可以访问外部类的私有成员。嵌套类通常用于实现某个类的内部辅助功能或数据结构。这可以帮助保持代码的整洁性,因为嵌套类的实现细节不会暴露给外部世界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Outer {
public:
class Nested {
public:
void nestedMethod() {
// 可以访问外部类的成员
}
};

private:
int privateMember;
};

int main() {
Outer::Nested nestedObj;
nestedObj.nestedMethod();
return 0;
}

写个双向链表的插入实现(gg)

哇,基础不牢,地动山摇

双向链表的实现,惭愧,这个基础没记住

当时写的这玩意儿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List* insert(List* head,int p,int q)
{
List* p1=head;
while(p1)
{
if(p1>val==p)
{
/*h->p<-q---->h
<- ->
*/
List* p2=new List(q);
/* p->end

*/

p1->end=p2;
p2->pre=p1;
p2->end=head;
p2=nullptr;
break;
}
p1=p1->end;
}
}

image-20230812222457210

以插入角度来讲,p后面加入q,我们考虑一下,在p与p->next结点中间插入q,需要满足什么

q结点与p->next结点先完成互指:

p->next->pre=q;

q->next=p->next;

p结点再与q结点完成互指:

p->next=q;

q->pre=p;

如果p是第一个节点怎么办捏

q->next=nullptr;

p->next=q;

q->pre=p;

如果p是尾结点

List* tem=p->pre;

p->pre=q;

p->next=nullptr;

tem->next=q;

q->pre=tem;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List* insert(List* &head,int p,int q)
{

List* p1=head;
while(p1->next)
{
if(p1->val==p)
{
List* qq=new List(q);
p1->next->pre=qq;
qq->next=p1->next;

qq->pre=p1;
p1->next=qq;

return head;
}
p1=p1->next;
}
}

完善判空,还有尾结点情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (!head) {
head = new List(0);
head->next = head;
head->pre = head;
return head;
}
List* p1=head;
while(p1->next)
{
if(p1->val==p)
{
List* qq=new List(q);
qq->next=p1->next;
qq->pre=p1;

if(p1->next){
p1->next->pre=qq;
}
p1->next=qq;

return head;
}
p1=p1->next;
}
return head;

来个判断字符串包含(gg)

哇,基础不牢,地动山摇

当时脑抽写了这个,啧

1
2
3
4
5
6
7
8
9
10
for(int i=0;i<s1.size();i++)
{
st.insert(s1[i]);
}
for(int i=0;i<s2.size();i++)
{
if(st.count(s2[i])) tem+=st->first;
}
if(tem==s2) return true;
return false;

之后口头的时候提到了substr函数的实现方式

1
2
3
4
5
for(int i=0;i<s1.size();i++)
{
if(s1.substr(i,s2.size())==s2) return true;
}
return false;

c++标准库函数有了解吗

stl嘛,容器那些,面试官只是回了:行

总结

空中楼阁;基础不牢,地动山摇

c++的应用,面试官说是很多,鸿蒙开发就是c;至于桌面应用是应用层的关系

面试官说,设计模式的重要性;基础掌握;思想逻辑;算法一定要知道基础;系统设计框架:生产者和消费者

来源

CSDN博主「丘比特惩罚陆」

知识补充来源:知乎笔记

为什么c++很少用于后端开发:

与java和python相比,c++缺乏像Spring和Django这种web框架,也没有一个像 J2EE 或 Servlet API 这样的标准。

c++适用于高性能和实时系统,如游戏开发、图形处理和科学计算等领域

而Web后端开发的重点是数据处理和业务逻辑

此外,web开发的工作是处理字符串,比如url,http头啊,输出的html、js啊,确实其它的语言,java、c#或者是python,处理字符串很容易,都有自带的库,随便调用一下,比如split这种方法。c++明显在这方面弱

说是没有自动垃圾回收器,即GC

然后嘞,web开发的都是解释性语言,c#,java等,解释性语言移植性要高些,c++这种编译性依赖特定平台

对计算量大又想高效的前后端应用,C++可能会合适。且Web Assembly已经全面落户最新的Chrome、Firefox、Edge、Safari等主流浏览器。可以将C++源码编译成wasm二进制文件直接在浏览器里高速运行(使用Emscripten编译更加便捷)。对很多前端应用来说,使用C++加速可能并没有必要。但是如果想要在前端产品中加入机器视觉,自然语言处理,机器学习等模块,WebAssembly将成为一个极好的选择。

知乎

sixclub

20230903

java的集合

首先对比数组的缺点

数组确定长度后,不可更改

增删元素的效率低

数组的实际数量无法获取,没有提供对应的方法或属性去获取

数组对于无序,不可重复的场合不能满足要求

对ArrayList的增删改查

add

remove

set(0,”eee”),下标为0的元素改为eee

get(2);

示例-音乐单(控制台版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
主程序:

import java.util.Scanner;
import java.util.ArrayList;
/*
音乐信息
编号 歌名 歌手 ——作为一个对象
*/
public class Test{
public static void main(String []args) {

//创建一个集合进行管理:存放歌曲对象
ArrayList list=new ArrayList();

while(true){
//菜单
System.out.println("————欢迎来到音乐单————");
System.out.println("1.展示");
System.out.println("2.添加");
System.out.println("3.删除");
System.out.println("4.退出");

//Scanner类
Scanner sc=new Scanner(System.in);

System.out.println("———请选择功能————");
//用键盘录入序号
int choice=sc.nextInt(); //录入int,回车,程序就可以接收到
System.out.println(choice);
//根据choice进行后续判断
if(choice==1){
System.out.println(">>>>1.展示");
for (int i=0; i<list.size();i++ ){
Music m=(Music)(list.get(i));
System.out.println(m.getmNo()+"---"+m.getmSong()+"---"+m.getmSinger()+"---");
}

}
if(choice==2){
System.out.println(">>>>2.添加");
//从键盘录入歌曲信息
System.out.println("请录入音乐编号:");
int mNo=sc.nextInt();

System.out.println("请录入音乐名:");
String mSong=sc.next();

System.out.println("请录入音乐人:");
String mSinger=sc.next();

//添加一首歌,就创建一个对象
// Music m=new Music();
Music m=new Music(mNo,mSong,mSinger);
// m.setmNo=mNo;
// m.setmSong=mSong;
// m.setmSinger=mSinger;
/*用上面这个,为什么cannot get symbol呢*/
//添加个体到集合
list.add(m);
}
if(choice==3){
System.out.println(">>>>3.删除");
}
if(choice==4){
System.out.println(">>>>4.退出");
break;
}

}

}
}

Music.java
public class Music{
private int mNo;
private String mSong;
private String mSinger;

public void setmNo(int mNo){
this.mNo=mNo;
}
public int getmNo(){
return mNo;
}

public void setmSong(String mSong){
this.mSong=mSong;
}
public String getmSong(){
return mSong;
}

public void setmSinger(String mSinger){
this.mSinger=mSinger;
}
public String getmSinger(){
return mSinger;
}

//初始化
public Music(){}

public Music(int mNo,String mSong,String mSinger){
this.mNo=mNo;
this.mSong=mSong;
this.mSinger=mSinger;
}
}

Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
1
1

1.展示
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
2
2
2.添加
请录入音乐编号:
1000
请录入音乐名:
yuleiyun
请录入音乐人:
yulei
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
1
1
1.展示
1000—yuleiyun—yulei—
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————

删除功能完善

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(choice==3){
System.out.println(">>>>3.删除");
System.out.println("请输入要删除的音乐编号");
int delNo=sc.nextInt();
for (int i=0;i<list.size() ;i++ ){
Music m=(Music)(list.get(i));
if(delNo==m.getmNo())
{
list.remove(i);
System.out.println("已删除");
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
>>>>1.展示
1---jk---jj---
2---yu---yyy---
3---1---12---
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
3
3
>>>>3.删除
请输入要删除的音乐编号
2
已删除
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
1
1
>>>>1.展示
1---jk---jj---
3---1---12---
————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
4
4
>>>>4.退出


** Process exited - Return Code: 0 **

java的I/O流

File类不涉及文件内容相关的操作

流这玩意儿,就是输入Input 和输出Output

按方向分:输入/输出流

按处理单元分:字节/字符流

按功能分:节点/处理流

FileReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
...
public static void main(String []args) throws IOException {
//将文件封装为具体的File类对象
File f=new File("1.txt");

//引入管子=输入字符流(将文件内容弄到程序里来)
//将管子怼到文件上
FileReader fr=new FileReader(f);
//“吸”
int n1=fr.read();
System.out.println(n1);

//流关闭
fr.close();



}
...
/*
1.txt内容为:
Yuleiyun,你好嘞
*/

89 //Y的Unicode 值噢

继续吸一个

1
2
3
4
 ...
int n2=fr.read();
System.out.println(n2);
...

89

117

“吸” 完了的话,后面的值就是-1了

所以,可以while读,将以上注释掉

1
2
3
4
5
6
7
 ...
int n=fr.read();
while(n!=-1){
System.out.println(n);
n=fr.read();
}
...

想要原内容,(char)转化一下即可

FileWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class Test {
public static void main(String []args) throws IOException {
//程序的字符串
String str="yly你好";

//文件
File f=new File("2.txt");
//流
FileWriter fw=new FileWriter(f);
//输出
fw.write(str);

//流的关闭
fw.close();
}
}

生成了一个2.txt,内容如下

yly你好

实例-音乐单(改)

集合版本缺点:不能将数据永久保存

FileI/O、ObjectI/O

序列化

添加功能

对于添加功能做如下改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
//创建一个集合进行管理:存放歌曲对象
ArrayList list=new ArrayList();
//添加个体到集合
list.add(m);

//将集合写进文件中去
File f=new File("1.txt");
//需要流
FileOutputStream fos=new FileOutputStream(f);
ObjectOutputStream oos=new ObjectOutputStream(fos);

//list写出
oos.writeObject(list);

//关闭
oos.close();
// fos.close(); //关掉最外层的即可
...

注释掉功能3和功能1的内容

运行程序,添加音乐对象时,报错

Exception in thread “main” java.io.NotSerializableException: Music

引出序列化,实现了序列化接口,才可以将对象输出到文件

为Music类,implements序列化接口

展示功能

更改

1
2
3
4
5
6
7
8
9
10
11
12
13
...
//从文件中读取
File f=new File("1.txt");
//流
FileInputStream fis=new FileInputStream(f);
ObjectInputStream ois=new ObjectInputStream(fis);

//list读入
//list--->从文件中读取集合
ArrayList list=(ArrayList)(ois.readObject());

//遍历查看
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>>>2.添加
请录入音乐编号:
1
请录入音乐名:
we
请录入音乐人:

————欢迎来到音乐单————
1.展示
2.添加
3.删除
4.退出
———请选择功能————
1
1
>>>>1.展示
1---we---为---

bug点

文件存在性

如果没有1.txt文件,但先执行展示功能,就会报错,所以:

判断是否有该文件,无则添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
。。。
if(f.exists()==false)
{
System.out.println("歌单为空,请先添加");
}
else{
//流
FileInputStream fis=new FileInputStream(f);
ObjectInputStream ois=new ObjectInputStream(fis);

//list读入
ArrayList list= (ArrayList)(ois.readObject());//list-->从文件中读取集合

//遍历查看
for (int i=0; i<list.size();i++ ){
Music m=(Music)(list.get(i));
System.out.println(m.getmNo()+"---"+m.getmSong()+"---"+m.getmSinger()+"---");
}
}
。。。
覆盖写

如果文件存在,即有内容:

从文件取出集合

为集合添加新内容

再将集合写进文件

否则:

声明一个空集合,添加内容

写进文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
。。。
//打开文件
File f=new File("1.txt");
if(f.exists()==true)
{
//文件存在,那么读完所有内容,再添加新内容
//流
FileInputStream fis=new FileInputStream(f);
ObjectInputStream ois=new ObjectInputStream(fis);


//list读入
//list-->从文件中读取集合
ArrayList list= (ArrayList(ois.readObject());
//更新集合
list.add(m);

//将集合写入
//需要流
FileOutputStream fos=new FileOutputStream(f);
ObjectOutputStream oos=new ObjectOutputStream(fos);

//list写出
oos.writeObject(list);

//关闭
oos.close();
// fos.close(); //关掉最外层的即可
}
else{//第一次添加

ArrayList list=new ArrayList();//空集合
list.add(m);
//需要流
FileOutputStream fos=new FileOutputStream(f);
ObjectOutputStream oos=new ObjectOutputStream(fos);

//list写出
oos.writeObject(list);

//关闭
oos.close();
// fos.close(); //关掉最外层的即可
}
。。。

删除功能

从文件中取出集合

从集合中删除对应项

将集合写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if(f.exists()==false){
System.out.println("歌单为空,请先添加");
}
else{
System.out.println("请输入要删除的音乐编号");
int delNo=sc.nextInt();
FileInputStream fis=new FileInputStream(f);
ObjectInputStream ois=new ObjectInputStream(fis);
ArrayList list=(ArrayList)(ois.readObject());
for (int i=0;i<list.size() ;i++ ){
Music m=(Music)(list.get(i));
if(delNo==m.getmNo())
{
list.remove(i);
System.out.println("已删除");
break;
}
}

//将list写出
FileOutputStream fos=new FileOutputStream(f);
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(list);

oos.close();
}

java的多线程

  • 程序是静态的(可执行文件);
  • 进程是程序的一次执行过程,资源分配的基本单位;

qq是个软件,那也是代码写的,我们称之为程序

双击启动它,

在内存中:

qq运行了,我们认为它是个进程

我们还可以双击它,再起一个qq。

  • 线程:进程进一步细化得到,是一个进程内部的一条执行路径
  • 线程是 操作系统 调度执行的基本单位

qq启动后,有ui吧,有的要刷新ui;要网络吧,有的要检测网络,有的要加载聊天记录吧,刷新qq好友吧。。。这些多条执行路径,对应多个任务,即线程

进程有多条执行路径,则多线程

一条主线程

线程创建

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callabel接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
子线程java中
package test;

/*
* 2.创建一个线程类
* 3.继承Thread获得多线程能力
* */

public class TestThread extends Thread{
//4.争抢资源,线程对应的任务,要放在run方法:


@Override
public void run() {
//5.线程任务:输出10个数
for (int i=1;i<=10;i++){
System.out.println("子线程---"+i);
}
}
}

主程序中
package test;

public class Test {
public static void main(String []args){
//1.main方法作为程序入口,执行的任务就是主线程的任务

for (int i=1;i<=10;i++){
System.out.println("main---"+i);
}

//6.创建子线程对象,并执行任务
TestThread t=new TestThread();
//7.执行任务,不直接调用run方法,而是启动线程
t.start();
//8.一旦子线程启动,系统会自动执行run方法,和主线程争抢资源

//9.主线程中再加入一个循环:
for (int i=1;i<=10;i++){
System.out.println("main--main---"+i);
}
}
}

main—1
main—2
main—3
main—4
main—5
main—6
main—7
main—8
main—9
main—10
main–main—1
main–main—2
main–main—3
main–main—4
main–main—5
main–main—6
子线程—1
main–main—7
main–main—8
main–main—9
main–main—10
子线程—2
子线程—3
子线程—4
子线程—5
子线程—6
子线程—7
子线程—8
子线程—9
子线程—10

Process finished with exit code 0

main–main—与子线程相互争抢

ps:看不出来可以调大它俩的i值,多启动几次,看交叉效果

main–main—7
main–main—8
子线程—1
main–main—9
子线程—2
main–main—10
子线程—3
main–main—11
子线程—4
main–main—12
子线程—5
main–main—13
main–main—14
main–main—15
子线程—6
main–main—16
子线程—7
子线程—8
子线程—9
子线程—10
main–main—17
main–main—18
main–main—19

java的JDBC

JDBC即 java数据库连接,SUN公司定义的一套接口

java程序->JDBC规范->MYSQL数据产商实现MYSQL驱动->MYSQL数据库

在程序中加入驱动包啊 mysql-connection.jar

下载链接

JDBC访问数据库编码步骤

  1. 加载Driver驱动
  2. 获取数据库连接
  3. 创建会话-SQL命令发送器(Statement)
  4. 通过Statement发送SQL命令并得到结果
  5. 处理结果
  6. 关闭数据库资源(ResultSet、Statement、Connection)

新建模块TestJDBC

新建文件夹lib,将jar包复制到此,右键jar包->add ..as Library

navicat事先建立mydb数据库,建立表为t_music,加3条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.test01;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1.加载驱动:
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
String url="jdbc:mysql://localhost/mydb?useSSL=false&useUnicod=true&characterEncoding = utf-8";

String uname="root";
String pwd="root";
Connection conn=DriverManager.getConnection(url,uname,pwd);

//3.创建会话
Statement sta=conn.createStatement();
//4.发送sql
int i=sta.executeUpdate("insert into t_music (Id,Song,Singer,Price) values (4,'哈哈','刘某','13')");
//5.处理结果
if(i>0){//对数据库条数有影响
System.out.println("插入成功!");
}
else{
System.out.println("插入失败!");
}
//6.关闭资源
sta.close();
conn.close();

}
}

结果如下:

刚才是添加的sql语句

删除

1
int i=sta.executeUpdate("delete from t_music where Id=2");

删除Id=2

更新

1
int i=sta.executeUpdate("update t_music set Song='Never say never',Singer='Yuleiyun' where Id=3");

更新Id=3的列

查询

1
2
3
4
5
6
7
8
9
10
//ResultSet结果集合-结果集
ResultSet rs= sta.executeQuery("select * from t_music");

//5.处理结果
while(rs.next()) {//判断是否有记录存在
//rs.getInt("Id");
System.out.println(rs.getInt("Id")+","+
rs.getString("Song")+","+rs.getString("Singer")+","+
rs.getInt("Price"));
}

1,天外来物,薛,0
3,Never say never,Yuleiyun,79
4,哈哈,刘某,13

也可对输出进行限制,比如 where Price > 20

实例-音乐单(数据库版)

用数据库对数据进行增删改查,比txt方便的多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package com.test01;

import java.io.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.Scanner;

public class Test01 {
public static void main(String []args) throws IOException, ClassNotFoundException, SQLException {

while(true){
//菜单
System.out.println("————欢迎来到音乐单————");
System.out.println("1.根据编号查歌");
System.out.println("2.返回所有歌");
System.out.println("3.删除指定编号的歌");
System.out.println("4.退出");

//Scanner类
Scanner sc=new Scanner(System.in);

System.out.println("———请选择功能————");
//用键盘录入序号
int choice=sc.nextInt(); //录入int,回车,程序就可以接收到

//根据choice进行后续判断
if(choice==1){

System.out.println("请录入想查看的歌曲编号");
int mNo=sc.nextInt();
Music m=findMusicByNo(mNo);

if(m!=null) System.out.println("查到该首歌为 \""+m.getSong()+"\"");
else System.out.println("此歌不存在!");
}

//显示所有
if(choice==2){
//添加歌,没成功
// System.out.print("请输入编号");
// int mNo=sc.nextInt();
// System.out.print("请输入歌名");
// String mSong=sc.next();
// System.out.print("请输入歌手");
// String mSinger=sc.next();
// System.out.print("请输入价格");
// int mPrice=sc.nextInt();
//
// //Music m=
// int i=st.executeUpdate("insert into t_music (Id,Song,Singer,Price) values ("+mNo+","+mSong+","+mSinger+","+mPrice+")");
// if(i>0) System.out.println("插入成功!");
// else System.out.println("插入失败!");
//
// st.close();
// conn.close();
ArrayList list=findall();
if(list.size()==0) System.out.println("歌单为0!");
else{
for (int i=0;i<list.size();i++){
Music m=(Music)(list.get(i));
System.out.println(m.getmNo()+","+m.getSong());
}
}
}
//删除
if(choice==3){
System.out.println("输入待删除编号");
int rNo=sc.nextInt();
int i=delSongbyNo(rNo);

if(i>0) System.out.println("删除成功!");
else System.out.println("删除失败!");
}
if(choice==4){
System.out.println(">>>>4.退出");
break;
}

}

}

//根据编号查询数据
public static Music findMusicByNo(int mNo) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");

String url="jdbc:mysql://localhost/mydb?useSSL=false&useUnicod=true&characterEncoding = utf-8";
String uname="root";
String pwd="root";
Connection conn=DriverManager.getConnection(url,uname,pwd);

//会话
Statement st=conn.createStatement();

ResultSet rs= st.executeQuery("select * from t_music where Id=" + mNo);

//但如果我只想传值,那有四个值,怎么传?
//那就封装到一个对象里面去
Music m=null;

if(rs.next()){

//接收值
int Id=rs.getInt("Id");
String Song=rs.getString("Song");
String Singer=rs.getString("Singer");
int Price= rs.getInt("Price");
//封装

m=new Music();
m.setmNo(Id);
m.setSong(Song);
m.setSinger(Singer);
m.setPrice(Price);
}

st.close();
conn.close();

return m;
}

//查询所有歌

public static ArrayList findall() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");

String url="jdbc:mysql://localhost/mydb?useSSL=false&useUnicod=true&characterEncoding = utf-8";
String uname="root";
String pwd="root";
Connection conn=DriverManager.getConnection(url,uname,pwd);

//会话
Statement st=conn.createStatement();

ResultSet rs= st.executeQuery("select * from t_music");

//定义集合
ArrayList list=new ArrayList();

while(rs.next()){

//接收值
int Id=rs.getInt("Id");
String Song=rs.getString("Song");
String Singer=rs.getString("Singer");
int Price= rs.getInt("Price");
//封装

Music m=new Music();
m.setmNo(Id);
m.setSong(Song);
m.setSinger(Singer);
m.setPrice(Price);

//放入集合
list.add(m);
}

//关闭资源
st.close();
conn.close();

return list;
}
//删除指定编号的歌
public static int delSongbyNo(int mNo) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");

String url="jdbc:mysql://localhost/mydb?useSSL=false&useUnicod=true&characterEncoding = utf-8";
String uname="root";
String pwd="root";
Connection conn=DriverManager.getConnection(url,uname,pwd);

//会话
Statement st=conn.createStatement();

int i=st.executeUpdate("delete from t_music where id="+mNo);

st.close();
conn.close();

return i;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
————欢迎来到音乐单————
1.根据编号查歌
2.返回所有歌
3.删除指定编号的歌
4.退出
———请选择功能————
2
1,天外来物
3,Never say never
4,哈哈
————欢迎来到音乐单————
1.根据编号查歌
2.返回所有歌
3.删除指定编号的歌
4.退出
———请选择功能————
1
请录入想查看的歌曲编号
4
查到该首歌为 "哈哈"
————欢迎来到音乐单————
1.根据编号查歌
2.返回所有歌
3.删除指定编号的歌
4.退出
———请选择功能————
3
输入待删除编号
4
删除成功!
————欢迎来到音乐单————
1.根据编号查歌
2.返回所有歌
3.删除指定编号的歌
4.退出
———请选择功能————
2
1,天外来物
3,Never say never

前端

HTML

文本 标记语言, 各种标签组成页面,经过浏览器解析,展示出来

1
2
3
4
5
6
<html>
<head></head>
<body>
<h1>Yuleiyun,欢迎诶!</h1>
</body>
</html>

CSS

层叠样式表,发现只有HTML,太单调了吧,CSS起装饰作用,修饰HTML页面

将上述文字变为红色嘞

内部样式:

1
2
3
4
5
6
7
8
9
10
<head>
<style type="text/css">

h1{
color:red;

}

</style>
</head>

外部样式:

mycss.css内容

1
2
3
4
h1{

color:blue;
}

html内容

1
2
3
<head>
<link rel="stylesheet" type="text/css" href="E:\渗透\mycss.css"/>
</head>

结果为,h1标签内容为蓝色

js

运行于浏览器的小脚本语句,通过事件驱动实现网页内容动态变化

内部js

1
2
3
4
5
6
7
8
9
head标签里添加
<script>
function f(){
alert("弹窗出来了喔");
}
</script>
body标签里添加
<input type="button" value="按钮诶" onclick="f()">

外部js

myjs.js

1
2
3
function f(){
alert("弹窗233333333333!");
}

html中

1
2
3
<script src="E:\渗透\myjs.js">

</script>

弹窗内容为 弹窗233333333333!

关系

HTML 形成初始页面

CSS 美化静态页面

js 添加事件,动态效果

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 是羽泪云诶
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信