学Unix时候的一篇笔记..

如题,虽然现在跟Unix毫无关系了,但毕竟也曾经为她痴狂过。发出曾经的笔记(其实之前还是纸质的,在面试之前我一边熟悉一边敲出来了),比较乱,权当作参考,希望对后来的童鞋有所帮助。




/****Witch的Unix笔记**********2010-12-3******/

课程定位   windows 基础

老师: 杨胖胖

课程体系

操作系统

语言  c/c++

面向对象编程(OOP面向对象编程)

OOA

数据结构与算法  容器的实现  算法的视线

STL  beyond of  STL  ---------boost

unix/unixc                                           windows

shell 批处理

  |

内存管理 kernel系统编程

kernel  系统编程 文件目录

IO  进程管理创建   IPC  线程 

线程同步

  |

应用编程

网络  UI  数据库编程 oracle pro* c/c++  MFC   ADO   D3D

ACE通信框架

 数据采集系统

操作系统API

标准API

time.h   math.h    stdio.h  

stdlib.h  string.h   errno.h

assert.h

第三方API

 

操作系统完成 

    1 驱动硬件

  2 内存管理

进程管理

文件目录管理

5 IO管理

课程顺序

1  shell编程

内存管理

文件与目录管理

4 linux内核中的IO

进程

6 IPC  进程通信

网络 socket编程  IO高级编程

多线程

多线程同步技术

10 QT 

11 PL/SQL 

12 PRO* C/C++

 shell编程

程序员和系统管理员

初步实现管理能力

2 shell编写用户脚本 用户的安装与配置脚本

shell实际上是一个程序,解释程序,负责将用户语法的语言解释成系统与内核调用的解释程序 

用户语法用最早的algol语言

shell 分为两类 1 algol语言风格  sh   ksh     bash

   2 c语言风格  csh   tcs

/bin/*sh

echo $shell显示当前是用的shell

只有类的成员函数才能声明为虚函数因为虚函数仅适用于有继承关系的类对象。

静态成员函数不能声明为虚函数,因为静态成员函数不受对象的捆绑,即使形式上的捆绑实际上也没有任何对象的信息,只有的类的信息。操作不受对象捆绑就失去了多态的条件,因为编译是在识别到对象的捆绑操作时开始滞后捆绑的,即多态是针对不同的对象,执行同一名称的操作,而能做出不同的选择机制。

内联函数不能是虚函数,因为内联函数是不能在运行中动态的确定其位置的,即使虚函数在类的内部定义,编译时仍然将它看作非内连的。

构造函数不能是虚函数 因为只有在对象构造完了之后才能成为一个名副其实的对象。

析构函数通常是虚函数。

进程通信

*匿名(只适用于父子进程):管道文件(1) socket文件(2)

**数据有序*

* *有名:socket文件(3)(不同主机间),管道 * 文件(4)(同一主机的不同进程)

**基于文件**

* *

* **数据无序:基于普通文件的共享映射(5

IPC**

*   **数据有序:共享队列(6)

*   * 

**基于内存**

  *         *共享内存(7)

  *数据无序*

        *基于匿名共享映射(8mmap(只能用于父子进程)

基于共享内存的同步技术:信号量(9

=============================================================

1)匿名管道文件(单工通信,只能从一端到令一端)

 #include <unistd.h>

  int pipe(int filedes[2]);

函数功能:

产生一对文件描述符,其中filedes[0]用于读,filedes[1]用于写.

工作流程:

1.定义一组或多组描述符

2.利用描述符创建匿名管道文件

3.通过每组的读写属性在父子进程间建立通信

例子:

#include<unistd.h>

#include<stdio.h>

main()

{

    int fd[2];

    int fd1[2];//这里定义了两组文件描述符,用于实现双工(也就是父子进程都既可以接收也可以发送)

    pipe(fd);

    pipe(fd1);

    if(fork())

    {

       close(fd[0]);//关闭读

       close(fd1[1]);//关闭写

       char buf[50] = {};

       while(1)

       {

          write(fd[1],"hello",5);

          read(fd1[0],buf,49);

          sleep(1);

          printf("%s\n",buf);

       }

    }

    else

    {

        close(fd[1]);//关闭写

        close(fd1[0]);//关闭读

        char buf[50] = {0};

        int r;

        while(1)

        {

          read(fd[0],buf,50);

          write(fd1[1],"world",5);

          sleep(1);

          printf("%s\n",buf);

        }

     }

  }

=============================================================

2)有名管道文件

#include<sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

函数功能:通过管道文件名和创建属性创建管道文件

参数:

pathname:管道文件路径下名字(不指名路径就是当前路径)

mode:权限

open

read/write

close

#include <unistd.h>

int unlink(const char *pathname);

函数功能:删除一个文件

工作流程:

1.在其中一个进程中创建管道文件

2.在两个进程中都打开该文件

3.一个进程读一个进程写

例子:

进程A

#include<stdio.h>

#include<sys/stat.h>

#include<unistd.h>

#include<stdlib.h>

#include<fcntl.h>

#include<signal.h>

char pipefile[] = "p.pipe";

int fd;

int r;

void closeproc(int s)

{

//4。关闭管道

close(fd);

//5。删除管道文件

unlink(pipefile);

exit(0);

}

main()

{

signal(2,closeproc);

//1。建立管道文件

r = mkfifo(pipefile,0666);

if(r)perror("mkfifo"),exit(-1);

//2。打开管道文件

fd = open(pipefile,O_RDWR);

if(-1 == fd)perror("mkfifo"),unlink(pipefile),exit(-1);

//3。每隔一秒,写入一个数据

while(1)

{

read(fd,&r,sizeof(int));

printf("::%d\n",r);

sleep(1);

}

//4。关闭管道

close(fd);

//5。删除管道文件

unlink(pipefile);

}

进程B

#include<stdlib.h>

#include<fcntl.h>

#include<signal.h>

int fd;

int r;

void closeproc(int s)

{

//4。关闭管道

close(fd);

exit(0);

}

main()

{

signal(2,closeproc);

fd = open("p.pipe",O_RDWR);

if(-1 == fd)perror("open"),exit(-1);

//3。每隔一秒,写入一个数据

int i = 100;

while(1)

{

write(fd,&i,sizeof(int));

i++;

sleep(1);

}

//4。关闭管道

close(fd);

}

============================================================

3)有名socket文件

1.socket函数说明

int socket(int domain,//插座目标

//目标1:本地文件

AF_LOCAL AF_UNIX AF_FILE

PF_LOCAL PF_UNIX PF_FILE

//目标2Internet

AF_INET

 int type,//数据传递格式

  //无边界流的格式

  SOCK_STREAM

  //有边界数据报文格式

  SOCK_DGRAM

 int protocol);0

  //大部分情况有参数1,2决定,只需要设置为0,系统只能选择协议

返回:>=0:成功返回 socket内存IDsocket描述符号

=-1:失败

2.bind函数说明

int bind(int fd,//socket描述符号

const struct sockaddr*addr,//绑定的目标地址

socklen_t len//地址的长度

)  

3.connect函数说明

int connect(int fd,//socket描述符号

const struct sockaddr*addr,//连接的目标地址

socket_t len)//地址长度

============================================================

工具使用

ipcs 显示共享内存(Shared Memory Segments),共享队列(Message Queues),共享信号量(Semaphore Arrays)

分别显示 ipcs -m

ipcs -q

ipcs -s

删除某一个   ipcrm -m   shmid

ipcrm -q   msgid

ipcrm -s   semid

============================================================

6)共享队列

#include<sys/msg.h>

#include<sys/ipc.h>

1.msgget函数说明

int msgget(key_t key,int flags)

flags分创建与打开

创建:IPC_CREAT|EXCL

打开:0

2.msgsnd函数说明

int msgsnd(int msgid

const void *msg,//消息缓冲

size_t size,//消息大小

int flags//消息发送方式

)

flags:

IPC_BOWAIT:碰到消息队列满,不发送直接返回

0: 等队列空了放入消息再返回

消息缓冲结构体格式:

struct xmsg

4字节: 消息编号

任意字节:消息内容

注意:发送消息的长度是消息的长度,不是结构体的长度,结构体的第一个元素是系统用来维护队列的。

3.msgrcv函数说明

ssize_t msgrcv(int   msqid, 

 void *msgp,//自定义结构体 

size_t msgsz, //返回缓冲的空间大小(结构体第二个数据的大小)

long msgtyp,//msgp里面的第一个参数相对应

 int msgflg//

);

4.int msgctl(int msqid, int cmd, struct msqid_ds *buf);

使用方法和共享内存中的一致

工作流程:

1.key 进程B

2.id 1.key

3.通过id发送消息到内存 2.id

4.删除id对应的内存 3.通过id接收消息从内存

例子:

进程A

#include<sys/msg.h>

#include<sys/ipc.h>

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<string.h>

//消息格式

struct charmsg

{

long type;

char data[100];

};

struct intmsg

{

long type;

int data;

};

main()

{

//1.得到key

//2.通过key获得id

key_t key = ftok(".",300);

int msgid = msgget(key,IPC_CREAT|0666);

//3.发送字符串消息

struct charmsg cmsg;

int i;

//  for(i = 0;i < 20;i++)

//  {

//  cmsg.type = 1;

//  bzero(cmsg.data,sizeof(cmsg.data));

//  sprintf(cmsg.data,"消息:%d",i);

//  msgsnd(msgid,&cmsg,strlen(cmsg.data),0);

//  }

struct intmsg imsg;

for(i = 0;i < 10;i++)

{

cmsg.type = 2;

imsg.data = i;

msgsnd(msgid,&imsg,4,0);

}

//4.发送整数消息

//5.删除内存

//sleep(20);

//msgctl(msgid,IPC_RMID,0);

}

进程B

#include<stdio.h>

#include<sys/msg.h>

#include<sys/ipc.h>

#include<stdlib.h>

#include<string.h>

struct charmsg

{

long type;

char data[100];

};

struct intmsg

{

long type;

int data;

};

main()

{

//1.生成对应的key

//2.通过key生成id

//3.接受消息

key_t key = ftok(".",300);

if(-1 == key)perror("ftok"),exit(-1);

int msgid = msgget(key,0);

if(-1 == msgid)perror("msgget"),exit(-1);

struct charmsg data;

int i;

//  for(i = 0;i < 10;i++)

//  {

//  ssize_t size = msgrcv(msgid,&data,sizeof(data.data),1,0);

//  data.data[size] = 0;

//  printf("%s\n",data.data);

//  }

struct intmsg idata;

for(i = 0;i < 10;i++)

{

ssize_t size = msgrcv(msgid,&idata,sizeof(idata.data),2,0);

printf("%d\n",idata.data);

}

}

===============================================================================================================

(7)共享内存

#include<sys/shm.h>

#include<sys/ipc.h>

1.shmget函数说明

int shmget(

key_t key,//唯一决定共享内存ID的生成

size_t size,//创建共享内存的大小

int flags//创建共享内存的方式与权限

flags:

两个部分构成:方式|权限

//方式:创建|打开(IPC_CREAT|IPC_EXCL/0

//

);

id是根据key产生的

返回共享内存ID

filetokey把文件路径转化成Key

key_t ftokconst char*filepath,int projid;

filepath:文件路径名,因为在某个路径下文件是唯一的所以产生的key也是唯一的。

projid:微调作用,可能这个路径下有几个程序同时用了这个路径,就需要用不同的projid来区分这几个不同的程序产生不同的key

2.shmat函数说明

void * shmat(int shmid,

const void *shmaddr, //NULL系统指定

int shmflg//指定挂载方式IPC_RDONLY或者0读写

)

返回:

挂载成功返回内存地址,0就为失败

3.shmdt函数说明

int shmdt(const void *shmaddr);

4.shmctl函数说明

作用:

控制(删除|获取共享内存的状态|修改共享内存状态的值)共享内存

int shmctl(int shmid,

 int cmd,

 struct shmid_ds *buf//只对IPC_STAT|IPC_SET有意义

);

cmd:

IPC_STAT//获取

IPC_SET//修改

IPC_RMID//删除

struct shmid_ds

{

               struct ipc_perm shm_perm;    /* Ownership and permissions */

               size_t          shm_segsz;   /* Size of segment (bytes) */

               time_t          shm_atime;   /* Last attach time */

               time_t          shm_dtime;   /* Last detach time */

               time_t          shm_ctime;   /* Last change time */

   pid_t           shm_cpid;    /* PID of creator */

               pid_t           shm_lpid;    /* PID of last shmat()/shmdt() */

               shmatt_t        shm_nattch;  /* No. of current attaches */

...

}

 struct ipc_perm {

               key_t key;            /* Key supplied to shmget() */

               uid_t uid;            /* Effective UID of owner */

               gid_t gid;            /* Effective GID of owner */

               uid_t cuid;           /* Effective UID of creator */

               gid_t cgid;           /* Effective GID of creator */

               unsigned short mode;  /* Permissions + SHM_DEST and

                                        SHM_LOCKED flags */

               unsigned short seq;   /* Sequence number */

           };

工作流程:

1.利用ftok创建一个唯一的key 另一个程序中:

2.通过key唯一产生一个id 1.通过相同的唯一文件产生相同的key

3.挂载内存,返回一个地址 2.通过key产生id也是和进程a相同的

4.操作该地址,修改地址指向的值 3.通过id挂载内存这时地址也是指向a内存的地址

5.解除挂载 4.读写该地址的内容

6.删除共享内存 5.解除挂载

例子:

进程A

#include<stdio.h>

#include<sys/ipc.h>

#include<sys/shm.h>

#include<string.h>

#include<stdlib.h>

main()

{

key_t key;

int shmid;

//1.根据目录创建key

key = ftok(".",255);

if(-1 == key)perror("ftok"),exit(-1);

shmid = shmget(key,4,IPC_CREAT|0666);//2.根据key唯一产生共享内存,返回其id

if(-1 == shmid)perror("shmget"),exit(-1);

printf("key:%d,id:%d\n",key,shmid);

//3.挂载内存

int *p = shmat(shmid,0,0);

if(!p)perror("shmat"),exit(-1);

//4.操作内存

*p = 9999;

//5.卸载地址

sleep(20);

shmdt(p);

}

进程B

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<string.h>

#include<sys/ipc.h>

#include<sys/shm.h>

main()

{

key_t key;

int shmid;

key = ftok(".",255);

shmid = shmget(key,4,0);

printf("id:%d,key:%d\n",shmid,key);

int *p = shmat(shmid,0,0);

printf("%d\n",*p);

sleep(20);

shmdt(p);

}

============================================================

9.信号量

#include<sys/ipc.h>

#include<sys/sem.h>

1.semget函数说明

int semget(key_t key,

int nsems,//创建的信号量的个数

int sem_flags//创建/打开信号量IPC_CREAT|IPC_EXCL,0

)

2.semop函数说明

int semop(int semid,

struct sembuf *op,//对信号量的操作

unsigned int nsops//第二个结构体数组的个数

)

struct sembuf

{

unsigned short sem_num//信号量下标

short sem_op;//信号量的操作

short sem_flg;//

};

信号量的操作

sem_op > 0:增加信号量(永远不阻塞)

sem_op < 0;减去信号量

够减:直接剪掉信号量sem_op,返回

不够减:阻塞,直到够减

sem_op = 0:信号量等于0则返回不阻塞,信号量大于0则阻塞

)buf

3.semctl函数说明

删除ID

修改信号量

获取信号量

修改信号量状态

int semctl(int semid,

int index,//信号量下标

int cmd,//信号量的操作命令

...   //对应命令的相关操作

)

cmd :

SETVAL:修改信号量的值(值不能小于0

值的格式使用union

例子 :

见项目信号量和共享内存实现的双向通信

=========================================

cc++的区别

1.c语言没有bool类型

2.c语言不支持引用

3.c语言不支持异常处理

4.c语言不支持面向对象

5.c语言不支持泛型编程

6.c语言不支持函数的重载

7.c语言不支持运算符的重载

stl总共有三种数据结构

list

每个节点都是由数据和指向下个节点的指针组成,节点间的地址不连续

不支持下标运算list只能根据前后节点进行访问,所以他用的跌代器是双向跌代器,不是随机的

arrayvector

数组结构,地址连续

支持下标运算,由于可以随机访问数组的任何元素,所以vector的迭代器是随机迭代器

dequedeque

上面两种数据结构的结合,既有数组也有链表数组

支持下标运算,,由于可以随机访问数组的任何元素,所以deque的迭代器是随机迭代器

list,vector,deque都是序列式容器

另外

stack,queue,没有迭代器,实现数据结构是deque,他们属于特殊容器操作方式有限

map,multimap,set,multiset,双向迭代器,都是关联式容器,树型结构,并且都排好序的,   其中只有map支持关键字下标运算

1.进程相关:

top 显示系统当前运行的所有进程的详细信息

pstree 显示进程树

ps aue 显示系统当前运行的进程信息(系统进程)

2.系统限制项

ulimit -a  (显示所有的)

3.进程间通信

ipcs -m memery

-q queue

-s  signal

-a  all

ipcrm -m shmid

nattch ---有几个程序在挂载

4.查看进程的存储结构

cat /proc/pid/maps

#include<unistd.h>

int write(int fd,//输出的文件描述符号

const void *buf,//输出数据的缓冲地址

size_t size//输出数据的长度

)

返回值

输出错误

>0 输出的实际字符个数

int read(int fd,

void *buf,//返回读取数据缓冲空间,用户分配

size_t size//

)

>0

  0 读取文件结束符,ctrl+d/网络关闭

-1:读取错误

====================================================================

#include<fcntl.h>

6.打开文件(可以设备文件/普通文件)

打开文件(可以设备文件/普通文件)

创建文件(只创建普通文件)

int open(const char *filename,//文件名

int flags,//打开|创建的方式

mode_t mode//只对创建文件有效

);

返回值:

>=0:创建打开成功,返回文件的文件描述符号

-1:打开失败

参数:

打开方式:

6.1.必须指定的标记(只能一个)

O_RDONLY O_WRONLY O_RDWR

6.2.创建与打开

O_CREAT 创建

不指定  打开

6.3.追加还是覆盖(对打开有效)

O_APPEND  追加

不指定    覆盖

6.4.清空(对打开有效)

O_TRUNC   清空

不指定    不清空

6.5.(对创建有效)(防止误删除存在的文件)

O_EXCL   文件存在,返回错误

不指定  文件存在,就直接清空,并打开 

总结:组合方式

1.

2.O_CREAT | O_EXCL

3.O_APPEND 

权限:

0777

案例1

创建文件,存在就直接返回 ,否则创建

,并且写入几个数据

20     4   1    4

"tom"  20 'm'  89.99

"jack" 19 'f' 99.99

2:读取两条记录

  7.判定标准设备的描述符号是否重新定向

*******#include<unistd.h>

   int isatty(int fd);

 案例:

  判定0 1 2是否指向终端

  判定普通文件描述符号是否指向终端

====================================================================

#include<unistd.h>

off_t lseek(

   int fd,//文件描述符号

   int off,//操作文件指针的位置

   int whence);//操作文件位置的参照点

   返回:

   文件指针操作后的绝对位置(从文件开始:0)

   参数:

   int whence:

   SEEK_SET:文件开始位置作为参照点

   指针位置>=0

   SEEK_CUR:文件当前位置作为参照点

   指针位置=0:不移动

   指针位置>0:向结束位置移动

   指针位置<0:向开始位置移动

   SEEK_END:文件结束位置作为参照点

   指针位置>=0

====================================================================

#include<sys/stat.h>

int  fstat(int fd,

struct stat *st);//返回文件信息

返回:

0成功

-1失败,并且修改errno

====================================================================

#include<unistd.h>

int ftruncate(int fd,

off_t length);//修改后文件长度

返回:

0:成功-1:失败

 读和写

读是指把自己的东西读到别的东西上  

scanf(),getchar(),gets()------把键盘输入到缓冲区的东西到某个变量上

istream(对象cin) --------------把 键盘输入到缓冲区的东西到某个变量上

ifstream(实例化一个对象)get()---------把文件的内容读到某个变量上

而写是把别的东西写到自己身上

printf(),putchar(),puts()-----把变量的值入到缓冲区中再显示在显示器上

ostream(对象cout)--------------把变量的值写入到缓冲区中

ofstream(实例化对象)put()-------把变量的值写入到文件中

读入    读----相对于自己,i ----相对于别人

写出    写----相对于自己,o-----相对于别人

几个内存操作的函数

p = malloc()

calloc()分配空间并初始化

p1 = realloc(p,n)在指定的地址上分配内存,如果P的大小够,就 取p的前面n个字节空间,不够的化就在内存中寻找大的内存块p内存要释放掉,地址空间首地址给p1

new 实际是调用malloc进行分配空间的,如果是类就会调用构造器

delete 实际是调用free释放空件,类就会调用析构器

delete[]会多次调用析构器,但是和delete一样都只调用一次free

======================================

#include<unistd.h>//unix系统函数

int brk(void *end_data_segment);

参数:

end_data_segment  是一个指向任何类型的指针,他通常指的是申请出来的内存空间的末尾地址。

返回值:0  success

-1-错误并设置errno

函数功能:申请堆内存空间,大小由指针控制如果要用brk释放申请的内存,就必须要保存首地址

void *sbrk(intptr_t increment);

参数:

increment   实际是整型数据,要申请空间的大小

返回值:

-1 ---错误并设置errno

 0 ----返回申请空间的首地址

注意:sbrk(0)会找到内存里面比较空闲的大块地址,一般情况下都是用sbrk(0)得到首地址,然后brk去申请空间

=======================================================================

#include<sys/mman.h>

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);

参数:

start:指向欲映射内存的起始地址,通常指定为NULL表示让系统指定,函数返回的也是这个地址

length:

表示将文件中多大的部分映射到内存

prot:

映射内存的保护方式

PROT_READ   映射区域可被读取

PROT_WRITE  映射区域可被写入

PROT_EXEC 映射区域可被执行

PROT_NONE 映射区域不能存取

要么是PROT_NONE要么是上面三中的组合

flags:

影响映射区域的各种特征,必须要指定MAP_SHAREDMAP_PRIVATE

MAP_FIXED:如果start所指的地址无法成功建立映射时,放弃映射,不对地址作修正。通常不用

MAP_SHARED:对映射区域的写入数据会复制会文件内,而且允许其他映射该文件的进程共享此数据(适用于要对文件进行写操作)

MAP_PRIVATE:对映射区域写入的操作会产生一个映射文件的复制,不会把对内存区域的修改写回文件(适用于只对文件进行读操作)

MAP_ANONYMOUS:建立匿名映射,此时会忽略参数fd不涉及文件操作,而且映射区域无法和其他进程共享

MAP_DENYWRITE:只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝

MAP_LOCKED:将映射区域锁定住,这表明该区域不会被置换

fd:要映射到内存中的文件描述符,如果使用匿名内存映射fd设为-1。不支持匿名映射的系统可以使用fopen打开/dev/zero文件,然后对该文件进行映射,同样可以达到匿名映射的效果。

offset:

文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页的整数倍(/getpagesize()==0)

以下例子可见MAP_SHAREDMAP_PRIVATE的区别

#include<sys/mman.h>

#include<fcntl.h>

#include<stdlib.h>

main()

{

//1.创建一个文件

//2.为该文件映射一段内存

//3.给文件一定的大小,不然直接用内存访问会出现总线错误

//4.利用映射后返回的指针操作文件

int fd = open("1.txt",O_RDWR|O_CREAT,0666);

if(-1 == fd)perror("打开或创建文件错误"),exit(-1);

void *p = mmap(0,getpagesize(),PROT_READ|PROT_WRITE,

/*MAP_PRIVATE*/MAP_SHARED,fd,0);

if((void*)-1 == p)perror("文件映射内存失败"),exit(-1);

printf("映射内存的首地址是:%p\n",p);

ftruncate(fd,100);

*((int*)p) = 10;1

printf("%d\n",*((int*)p));

close(fd);

}

MAP_PRIVATE的时候即使利用指针改变了p指向的值但是文件1.txt里面依然是空白的,说明了不会把映射区域修改的内容写回文件,但是如果改成了MAP_SHARED,

结果1.txt里面就会有10(当然直接看是看不到的涉及到格式问题,如果是写入字符的话就可以很清楚的看到了)

int munmap(void *start,size_t length);

释放指定地址地址的内存空间

====================================================================

继承的属性

总结:继承过来的所有的父类的public属性全都变成了继承方式的属性

所有的私有的属性都变成不可访问,所有的保护类属性都变成了函数可访问,对象不能访问

注意:

要用初始化列表来初始化继承来的属性

静态动态库的加载运行

假设有

diamond.c        inputr.c            ninemulnine.c

activemain.c-----利用动态链接库接口访问 main.c----普通连接访问库

总结:

.加载静态库的执行过程

1.gcc -c diamond.c inputr.c ninemulnine.c

  生成obj文件diamond.o  inputr.o  ninemulnine.o

2.ar r libstatic.a *.o  

将三个obj文件插入到归档文件libstatic.a中(也就是静态库)

3.gcc main.c -lstatic -L. -o main

将几个文件连接起来,形成可执行文件main

.加载动态链接库的执行过程

1.gcc -c diamond.c inputr.c ninemulnine.c

  生成obj文件diamond.o  inputr.o  ninemulnine.o 

 这一步是相同的,都是把函数包装好

2.gcc -shared *.o -o libauto.so

将函数封装到动态链接库中

3.gcc main.c -lauto -L. -o main1

将几个文件连接起来,形成可执行文件main1

.利用标准库提供的函数访问动态库函数

1.#include<dlfcn.h>//包含头文件

2.定义句柄保存动态库的首地址void *dlopen(const char *filename, int flag);

3.定义函数指针保存要调用函数的首地址

void *dlsym(void *handle, const char *symbol);

    4.操作函数指针

5关闭句柄  int dlclose(void *handle);

例子:

#include<dlfcn.h>

#include<stdio.h>

#include<stdlib.h>

main()

{

void *p = dlopen("libku.so",RTLD_NOW);//加载动态库

if(!p)printf("加载失败!\n"),exit(-1);

int (*fun)(int,int) = dlsym(p,"add");//查找动态库中的函数

if(!fun)printf("查找失败!\n"),exit(-1);

printf("%d\n",fun(1,3));

dlclose(p);

}

默认构造函数     --- 构造对象时      

默认拷贝函数 ----用于同类型对象初始化时

Aconst A&;//拷贝构造函数,同类型对象 初始化时

A a ;

A b = a;//等价于A b(a);

AB& b;

B b;

A a = b;//像隐式类型转换

1.参数的值传递

2. 函数里作为临时值返回,看具体情况

析构函数        ---- 用了NEW 的一定要自己定义析构函数

默认赋值运算符 ---用于赋值,和拷贝唯一的不同

eg:

class A

{

int a1;

int a2;

int *array;

A()

{

array = new int[100];

}

A(const A& a)//默认拷贝

{

a1 = a.a1;

a2  = a.a2;

array = a.array;//指向了同一块内存空间,释放时释放了两次,并且操作同一空间时会产生混乱,必须自己写

可以写成

array = new int[100];

再一个一个赋值

}

operator=(const A& a)//默认赋值

{

      a1 = a.a1;

a2  = a.a2;

}

}

初始化时都会调用构造函数,如果是初始化时赋值可以调用拷贝构造函数或者普通构造函数,会视型参的类型进行匹配

类的特化偏特化

1.模板参数必须是类型,其他参数只能为INT 类型

2.模板参数可以有默认值,并且只能从右边开始给默认值,和函数默认值是一样的

注意:但是函数模板不能给默认值

3.每一个模板声明只能对应一个类或函数,在函数定义在类外在每个函数前也必须加上模板声明

4.类模板函数的声明和定义必须放在同一个文件中,最好像标准模板库那样写成.h文件

*****如果要放在两个文件中就必须不允许类模板中存在类或结构体*******

5.函数模板有类型推断,类模板没有,就算类模板有默认的模板参数也要写个<>空的上去

有一种情况下函数模板也没有类型推断

template <typename T>

T *f2()

{

return new T();

}

int *p = f2();//error

int *p = f2<int>()//right

就是没有传进去任何参数,编译器就无法推断出类型

6.模板类的特化

当处理const char*或者其他类型时可能会出现一些问题这时候就需要把这些问题单独的些一个特化的类来解决这个问题

template<>class 类名<char*>{};-------完全特化

template<class T> class Vector<T*>{};-------部分特化,偏特化

编译器先找完全特化再找部分特化最后才找模板

7.模板函数和模板函数可以形成重载但是不能有歧义

进程描述

什么是进程:

执行中的程序。进程包含:代码,资源,CPU占用

linux怎么管理进程

task_struct 树管理进程:每个节点是进程描述

怎样描述进程:

进程描述有很多属性构成描述

信号操作函数

老版本的信号操作函数

======================================================================

#include<signal.h>

typedef void (*sighandler_t)(int);//将函数指针重定义为sighandler_t

sighandler_t signal(int signum,sighandler_t handler);

函数功能:

接受信号;

参数:

signum---信号的编号,或宏

handler---处理接受到的信号的函数,可以屏蔽信号

返回值;

handler的值是一样的

=========================================================================

#include<signal.h>

int kill(pid_t pid,int sig);

向指定的进程pid发送信号sig

pid > 0向指定的进程pid发送信号

pid == 0向进程组的任何进程发送信号

pid == -1向所有的进程发送信号

pid < -1向进程组ID|pid|的所有进程发送信号(||是取绝对值)

=========================================================================

新版本的信号操作函数

=========================================================================

#include<signal.h>

int sigaction(int signum,

const struct sigaction*act,//处理函数及其相关信息的设置

struct sigaction*oldact);

struct sigaction结构体说明

{

void (*sa_handle)(int);信号处理老版本函数

void (*sa_sigaction)(int,siginfo_t *,void *);

sigset_t sa_mask,//上面两个中断函数在执行的时候被屏蔽的信号

int sa_flags;//控制到底优先使用哪个版本的信号处理哪个函数

0优先使用老版本,SA_SIGINFO则使用新版本的函数

void (*sa_restorer)(void);//永远不使用的成员

}

例子:day08/代码/sigaction.c

======================================================================

#include<signal.h>

sigqueue发送信号(kill

int sigqueue(pid_t pid,int sig,

const union sigval value);

union sigval

{

int  sival_int;//这个是和sigaction结构体中的siginfo_t相对应的

void* sival_ptr;//这个和void*对应

}

参数:

pid--接受方的进程id

sig---要发送的信号

value---发送信号时顺带发送过去的内容(只适用简单数据)

列子:day08/代码/sigactionA.c

=====================================================================

#include<signal.h>

int sigprocmask(int how,//信号操作方式   SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK

const sigset_t *set,//操作的信号集合

sigset_t *oldset//返回原来设置的信号

)

sigset_t是一个结构体

函数功能:

对信号集合进行操作,包括阻塞,非阻塞和SIG_SETMASK设置为0

返回:

0:成功

-1:失败

=====================================

#include<signal.h>

int sigpending(sigset_t *sigs);

参数返回接受到的屏蔽信号

信号处理函数只屏蔽本生的中断信号

在中断函数执行的时候屏蔽函数不受其他中断的影响

函数功能:

判定是否有屏蔽信号在队列中

sigismember联合使用,可以判断某个被阻塞的信号是否已经发送出来

=====================================

#include<signal.h>

信号集合的操作(一组函数)

int sigemptyset(sigset_set *set)(信号集合制空)

int sigdelset(sigset_set *set,int sig)

int sigaddset(sigset_set *set,int sig)

int sigfillset(sigset_set *set)(62个信号全部放进去)

int sigismember(sigset_set *set,int sig)(判断一个信号是否在集合内)

异步IO

多进程实现

父子进程

特点:

父进程和每个子进程时刻都在监听,等待数据(同步)

异步IO

只需要一个数据结构维护所有服务器和客户的文件描述符,有改变时才进行监听,读写数据(异步)

read是一个阻塞函数

int select(

int nfds,//监视的最大描述符号(循环监视的)+1.不是序号而是值

fd_set ifds,//要监视的输入描述符号:缓冲有数据()

fd_set ofds,//监视的输出描述符号:缓冲没有数据

fd_set efds,//监视的错误输出描述符号:缓冲无数据

struct timeval* timeout//延时设置      

)

返回值:

0:表示到了指定时间

1:失败

>0:成功,发生改变的文件描述符的个数(输入:返回有数据的缓冲个数,输出:返回缓冲为空的个数)

注意:

第二个参数:即是输入参数,也是输出参数

输入要监视的描述符号

输出的是有数据 变化的描述符号

如果没有文件描述符发生改变的时候fds就会置为NULL

selectmaxfd+1,&fds,0,0,0

fds集合里面的文件描述符发生了改变(即缓冲有数据);

if(如果是服务器的fd改变----有人连接服务器)

{

accept  得到客户fd并且

}

else(如果客户的fd发生改变------向输入缓冲写入了数据)

read  读取客户写入的数据

函数参数传指针实际上也是传的值,只不过值是一个地址

传引用传的就是地址

调用拷贝构造函数的一种情况就是对象以值的方式传进函数,但是必须是对象的值而不是地址值

#include <stdio.h>

#include <stdlib.h>

#include <dlfcn.h>

int main()

{

int (*padd)(int,int); 

      void *h;

h=dlopen("libku.so",RTLD_LAZY);

if(!h) printf("鍔犺浇澶辫触!\n"),exit(-1);

padd=dlsym(h,"add");

if(!padd) printf("鏌ユ壘澶辫触!\n"),exit(-1);

     int r=padd(45,55);

    printf("::%d\n",r);

   dlclose(h);

 return 0;

}

=====================day04============================================ ======================================================================

1.函数指针

2.我们学习哪些内存管理知识

mmap/munmap

brk/sbrk

malloc/free/calloc/realloc

new/new[]/delete/delete[]

STL内存管理Allocator

智能指针

.函数指针

1.函数指针:

返回类型(*)(参数列表);

2.声明函数指针变量

返回类型(*函数指针变量)(参数列表);

3.函数赋值

函数指针变量=函数指针

4.函数指针类型转换

(函数指针类型)

5.函数指针调用

变量=函数指针变量(实参列表)

6.typedef与函数指针

Linux:size_t  ssize_t  off_t  pid_t

7.函数栈产生的规律

函数参数的压栈有顺序的。

函数添加修饰属性 __stdcall  __cdecl  __fastcall

控制函数的参数压栈顺序,以及访问地址范围

决定函数编译时候的命名规则

__attribute__((stdcall))

__attribute__((cdecl))

__attribute__((fastcall))

.内存管理

mmap/munmap

brk/sbrk

malloc/free/calloc/realloc

new/new[]/delete/delete[]

STL内存管理Allocator

智能指针

1.认识各种不同的内存

代码区

全局区

局部区

堆区

实验1

直观查看各个内存段。

/proc/$PID/maps文件动态映射进程的内存结构

/proc/$PID在进程结束以后,自动删除.

1000 =4Kbyte=4096byte=1page

注意:/proc文件系统暴露系统内核的相关信息。

Linux中程序代码空间地址永远在08048000

0-3G应用程序使用的空间

3G-4G内核init使用的空间3G-3G+8M

实验2

体会各种变量在内存中的位置

实验3

栈地址的变化(从大变小)

堆地址的变化(从小变大)

问题:

栈分配空间是实际大小

堆分配的空间是实际大小+12字节

2.mallocnew分配空间的差别

2.1.malloc/new怎么分配空间(虚拟内存)

用户申请一个空间,系统查找大块没有使用的空闲的地址.

并且关联一个物理空间(映射)

实验

体会虚地址/虚拟内存的概念

结论:

每个进程都有一个虚拟的从0-4G地址。

平面内存.flat memory

虚拟内存映射物理内存采用

段页管理

段:把地址转换为线性地址

页:完成实际物理映射,一般为了处理碎片内存,页分成3-4

2.2.malloc/new对分配的空间怎么管理?

实验:

体会malloc对内存是有管理的。

a.栈是栈结构管理,malloc采用堆的方式管理。

堆的管理采用是链表容器来管理。

链表的节点:上一个内存节点地址

 下一个内存节点地址

 内容内存空间

 内存大小

 

b.malloc按页申请空间

2.3.堆内存访问的规则:

不允许的访问:

段错误访问(能否访问)

非法访问 (该否访问)

识别是否非法访问破坏内存管理结构。

识别什么情况下会段错误。

不要胡乱使用指针运算。

+n  -n +=n -=n  ++  --  []

if(p) *p 

2.4.C++ C内存操作的区别

相同点:

new :malloc  calloc 

delete  free  realloc

new[] :calloc  malloc

delete[]free

new()   realloc  replacement alloc

区别点:

delete 调用一次析构器

delete[] 调用多次析构器

new调用一次构造器

new[]调用多次构造器

int *a=new int[30];

delete a; ~a(),free(a);

delete[] a; ~a[0]() ~a[1]()...,free(a);

free  /delete  /delete[]区别

不调用/调用一次/调用多次

3.C是否可以像栈一样来自己管理内存。

brk/sbrk

练习:

1-10000之间所有孪生素数

孪生素数:连个之间差的绝对值<=2

1.函数说明:

int brk(void *p);

作用:

分配/释放空间

参数:

void*p指定有效地址末尾范围

返回:

0

内存分配释放成功

-1或者ENOMEM

内存分配失败

void *sbrk(intptr_t increment)

作用:

分配释放空间

参数:

要分配空间大小

返回:

分配空间的首地址

2.理解brk/sbrk函数

malloc在后台维护一个数据结构链表。

brk/sbrk在后台维护数据:开始地址、结束地址

sbrk(0)返回一个开始地址.地址在页首:4k的倍数

实验:

使用sbrk返回空闲的首地址!但不映射物理空间

sbrk参数可以

>0:分配空间,返回首地址

=0:不动,返回首地址

<0:释放空间,返回首地址

建议不要使用sbrk释放空间.

建议使用sbrk返回首地址。而是使用brk改变末尾地址实现分配与释放

案例:

查找1-10000之间所有素数,保存到内存,查找完毕

打印所有素数。释放空间

注意:分配空间

使用空间

释放空间

问题:

int a[20][20];

a[10] -a[1];返回个数

int *p=new int[20][20];

p[10]-p[1];返回地址字节数

.内存映射

映射相对底层的内存管理方式。

控制内存的权限。

控制内存映射到文件

1.函数说明:

a.mmap映射内存

void mmap(

void *start,//从指定位置开始映射,必须是页首

//如果为0/NULL,系统指定开始映射地址

size_t size,//映射的大小,建议page倍数

int prot,//指定映射权限

//PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE

int flags,//映射方式

//MAP_SHARED MAP_PRIVATE

//MAP_ANONYMOUS 内存映射。否则是文件映射

int fd,//只对文件映射有效

off_t off//问价你映射的开始位置,必须是page的倍数

);

b.mumap卸载映射

void munmap(void *start,size_t length);

练习:

1.使用brk/sbrk打印1-20000之间的孪生素数.

2.使用new/malloc/brk/mmap分配空间,观察分配规律

3.使用new/malloc/brk/mmap分配空间,观察/proc/$pid/maps

每个内存在那个段?

思考:

4.理解内存.

#include <stdio.h>

#include <unistd.h>

main()

{

int *p=sbrk(4);

int *p2=sbrk(4);

int *p3=sbrk(4);

printf("%x\n",p);

printf("%x\n",p2);

printf("%x\n",p3);

}

用sbrk判断素数

#include <stdio.h>

#include <unistd.h>

int isprimer(int a)

{

int i;

for(i=2;i<a;i++)

{

if(a%i==0) return 1; 

}

return 0; 

}

main()

int *pstart=sbrk(0);

int *p; 

p=pstart;

int i;

for(i=2;i<10000;i++)

{

if(!isprimer(i))

{

brk(p+1);

*p=i;

p=sbrk(0);

}

}

int *pend=sbrk(0);

p=pstart;

while(p!=pend)

{

printf("%d\n",*p);

p++;

}

brk(pstart);

}

函数指针

#include <stdio.h>

int add(int a,int b)

{

return a+b;

}

int sub(int a,int b)

{

return a-b;

}

main()

{

typedef int (*addfunc)(int,int);

printf("%x\n",main);

printf("%x\n",&main);

int (*padd)(int,int);

padd=(int(*)(int,int))add;

addfunc a;

a=add;

/*

padd=add;

padd=*add;

padd=&add;*/

int r=padd(45,55);

printf("%d\n",r);

}

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/mman.h>

#include <string.h>

/*

#define NULL (void*)0

*/

main()

{

char*str=mmap(

0,//

1*getpagesize(),//

PROT_READ|PROT_WRITE,//

MAP_SHARED|MAP_ANONYMOUS,//

0,0); 

memset(str,0,1*getpagesize());

memcpy(str,"Hello World!",12);

printf("%s\n",str);

munmap(str,1*getpagesize());

}

int  *p=(int*)realloc(c,20);

#include <stdio.h>

#include <unistd.h>

main()

{

int *p=sbrk(0);

brk(p+1);

*p=10;

brk(p);

*p=20;

============================day05============================================================================================================================

一.UnixC环境变量

1.main函数

int main(int args,char**argv,char**arge)

{

}

重点:

双指针最常见的用途:

a.表示字符串数组

b.在函数参数中用来返回指针

结论:环境变量的字符串数组与命令行参数数组最后一个必须是0指针或者NULL

2.environ变量

与main的arge作用相同

setenv修改某个环境变量

getenv获取某个环境变量

3.异常处理

3.1.C异常处理采用返回值指示错误信息

C的函数返回值:

返回指针:非0表示成功 ,0表示失败

返回整数:0表示成功,>0表示成功,-1表示失败

详细错误:

在C中错误使用编号errno

错误描述:

strerror把errno转换成错误文本描述

perror直接打印错误文本

void perror(const char*);

printf("%m")直接打印错误文本描述

3.2.errno修改规则:

a.哪些修改errno

主要根据man帮助文档知道函数是否修改errno

b.函数怎么修改errno

函数没有错误的时候,是不访问errno的。

语句错误

语句正确

访问errno 是第一个语句的错误不是第二个。

结论:函数调用后,马上访问errno.

补充man手册的使用:

man就是查找程序,在/usr/man  /usr/local/man  /usr/.../man

man1 man2 man3   ... man7 man8  man9

man 节 关键字

节:section 1-8

1:shell指令

2:系统函数

3:标准C函数

7:网络综合帮助

9:内核函数帮助文件

4.printf与sprintf的使用

% 零或者flags 宽度 精度 长度修饰符号  转换指示符号

 4.1.零或者flags

  #自动判定格式

  0数据实际长度与输出长度不足下补0

  空格,自动补空格  

  -对齐方式 

  +输出+-符号  

 4.2.宽度

  数字

  *

  *m$:m是数字:m指定参数的位置,从1开始

 4.3.精度

  .数字

  .*

  .*m$

 4.4.长度修饰符号

  hh  h  l  ll

 4.5.转换指示符号

  %m 打印errno对应的错误描述字符串

  %n 返回前面输出的数据长度

 默认右对齐,-左对齐  

课堂练习:

输出YYYY年MM月DD日

  scanf

  sscanf

二.IO

回顾IO的技术

标准C++IO

标准C IO

作业:

找出FILE在那个头文件中?

系统相关的IO

/dev/sda1 /dev/sda2

/dev/pts/1

/dev/zero

/dev/null

/dev/tty

网络/文件/目录/内存都可以采用IO方式访问

1.IO系列相关函数

read 

write

open

close

lseek

pread

pwrite

fstat

ftruncate

2.理解文件描述符号

任何设备文件必须打开在系统内核存在对应内存区域

禁止用户直接访问设备呢?

把内核中内存块的地址隐藏起来,返回用户一个编号:4字节整数.

用户靠指定编号操作内存,从而操作设备。

把整数编号称为文件描述符号

file descriptor

3.标准设备IO

标准设备/dev/tty  /dev/pts/1...

每个进程在创建的时候自动打开标准设备

0:标准输入 /proc/$pid/fd/0

1:标准输出 /proc/$pid/fd/1

2:标准错误输出 /proc/$pid/fd/2

4.write的使用

函数的说明:

int write(int fd,//输出的文件描述符号

const void *buf,//输出数据的缓冲的地址

size_t size); //输出数据的长度

返回值:

-1: 输出错误

>0:  输出的实际字符个数

5.read的使用

函数说明

int read(int fd,

void *buf,//返回读取数据缓冲空间,用户分配

size_t size)//存放返回数据的空间大小

返回值:

>0:实际读取的数据

0:读取文件结束符号CTRL+D/网络关闭

-1:读取错误

6.打开文件(可以设备文件/普通文件)

打开文件(可以设备文件/普通文件)

创建文件(只创建普通文件)

int open(const char *filename,//文件名

int flags,//打开|创建的方式

mode_t mode//只对创建文件有效

);

返回值:

>=0:创建打开成功,返回文件的文件描述符号

-1:打开失败

参数:

打开方式:

6.1.必须指定的标记(只能一个)

O_RDONLY O_WRONLY O_RDWR

6.2.创建与打开

O_CREAT 创建

不指定  打开

6.3.追加还是覆盖(对打开有效)

O_APPEND  追加

不指定    覆盖

6.4.清空(对打开有效)

O_TRUNC   清空

不指定    不清空

6.5.(对创建有效)(防止误删除存在的文件)

O_EXCL   文件存在,返回错误

不指定  文件存在,就直接清空,并打开 

总结:组合方式

1.

2.O_CREAT | O_EXCL

3.O_APPEND 

权限:

0777

案例1:

创建文件,存在就直接返回 ,否则创建

,并且写入几个数据

20     4   1    4

"tom"  20 'm'  89.99

"jack" 19 'f' 99.99

2:读取两条记录

  7.判定标准设备的描述符号是否重新定向

   int isatty(int fd);

 案例:

  判定0 1 2是否指向终端

  判定普通文件描述符号是否指向终端

 

  8.怎么使程序的输出不被定向到文件

  解决办法:

  不使用缺省的0 1 2 ,而是直接打开终端设备文件/dev/tty,直接输出,不要输出到伪设备文件  

  9.读取一个特殊的文件/proc/$pid/mem

 

  10.umask的权限遮罩影响创建的权限

  umask

  umask 权限

 

总结:

1.对标准IO读写

2.对文件IO读写(基本类型的读写【字符串】,字符,整数,小数)

作业:

1.循环从键盘输入图书信息,并保存到文件,每条记录输入完毕,提示是否继续输入。

图书信息的记录:

书名,作者,价格,存量

2.写一个程序,查询某个作者的书的信息。

程序名  作者

 

 

 

 

环境变量

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

extern char**environ;

main(int args,char**argv,char**arge)

{

printf("%s\n",getenv("PATH"));

printf("%s\n",getenv("LOGNAME"));

/*

while(environ && *environ)

{

printf("%s\n",*environ);

environ++;

}

*/

/*

while(arge && *arge)

{

printf("%s\n",*arge);

arge++;

}

int i=0;

while(arge && arge[i])

{

printf("%s\n",arge[i++]);

}

*/

}

read &&open 函数

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <fcntl.h>

#include <string.h>

main()

{

char name[20];

int  age;

float score;

char sex;

char *filename="a.dat";

int fd;

fd=open(filename,O_RDWR);

if(fd==-1)

{

perror("open error");

exit(-1);

}

int r;

while(1)

{

r=read(fd,name,sizeof(name));

if(r<=0) break;

r=read(fd,&age,sizeof(int));

r=read(fd,&score,sizeof(score));

r=read(fd,&sex,sizeof(char));

printf("%s,%d,%5.2f,%c\n",

       name,age,score,sex);

}

close(fd);

}

write&&函数 

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <stdlib.h>

#include <string.h>

main()

{

int fd;

char *filename="a.dat";

char name[20];

int  age;

float score;

char sex;

  fd=open(filename,O_RDWR|O_CREAT|O_EXCL,0666);

if(fd==-1)

{

perror("open error");

exit(-1);

}

bzero(name,sizeof(name));

//memset(name,0,sizeof(name));

memcpy(name,"tom",3);

age=20;

score=89.99f;

sex='m' ;

write(fd,name,sizeof(name));

write(fd,&age,sizeof(age));

write(fd,&score,sizeof(score));

write(fd,&sex,sizeof(sex));

bzero(name,sizeof(name));

memcpy(name,"Jack",4);

age=18;

score=99.88f;

sex='f';

write(fd,name,sizeof(name));

write(fd,&age,sizeof(age));

write(fd,&score,sizeof(score));

write(fd,&sex,sizeof(sex));

close(fd);

}

printf("%*2$.*3$f\n",12345.6789,40,20);


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值