【跟着陈七一起学C语言】今天总结:函数、数组、指针之间的关系

news2024/11/16 21:37:52

友情链接:专栏地址

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家

最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上


文章目录

  • 🚀指针与函数
    • ⛳一、函数返回值使用指针
    • ⛳二、指针变量作为函数参数
      • 🎈(一)数组传参
      • 🎈(二)指针传参
    • ⛳三、函数指针
      • 🎈1.声明函数指针
      • 🎈2.用函数指针访问函数
      • 🎈3.函数指针作为函数参数
  • 🚀指针与数组
    • ⛳一、指针表示法和数组表示法
    • ⛳二、指针和多维数组
    • ⛳三、指针数组
    • ⛳四、数组指针
    • ⛳五、数组和指针的联系


🚀指针与函数

⛳一、函数返回值使用指针

可以返回函数内部:

  • 动态分配内存地址
  • 局部静态变量地址
  • 全局静态变量和外部变量地址
// demo 9-8.c
#include <stdio.h>

int * add(int x, int y)
{
    int sum = x + y;
    return &sum;
}

//返回动态内存分配地址
int * add1(int x, int y)
{
    int * sum = NULL;
    sum = (int*)malloc(sizeof(int));
    *sum = x + y;
    return sum;
}

//返回局部静态变量的地址
int * add2(int x, int y)
{
    static int sum = 0;
    printf("sum: %d\n", sum);
    sum = x + y;
    return &sum;
}

int main()
{
    int a = 3, b = 5;
    int *sum = NULL;
    
	//不能使用外部函数局部变量的地址 bad
    sum = add(a, b);
    
    //接收外部函数动态内存分配的地址 ok
    sum = add1(a, b);

    //接收外部函数局部静态变量的地址
    sum = add2(a, b);

    *sum = 88888;
    add2(a, b);
    
    system("pause");
    return 0;
}

函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。

⛳二、指针变量作为函数参数

函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。

编写一个处理基本类型(如,int)的函数时,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。

🎈(一)数组传参

1.数组传参时,会退化为指针!

  • C 语言只会以值拷贝的方式传递参数,参数传递时,如果拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。
  • 因此,C 语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址

对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。

传递地址会导致一些问题。C 通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。有时,这正是我们需要的。例如,下面的函数给数组的每个元素都加上一个相同的值:

void add_to(double arr[], int n, double val)
{
 int i;
 for (i = 0; i < n; i++)
 	arr[i] += val;
}
//调用该函数后,prices数组中的每个元素的值都增加了2.5:
add_to(prices, 100, 2.50);

该函数修改了数组中的数据。之所以可以这样做,是因为函数通过指针直接使用了原始数据。

2.用数组的形式传递参数,不需要指定参数的大小, 因为在一维数组传参时,形参不会真实的创建数组, 传的只是数组首元素的地址。当然:写上一个数组大小也是可以的,方便自己知道这个数组有多大,在函数代码中也写上固定的数组大小,其它并无多大用处,常用法还是将数组原大小新增一个参数传递进来

void method_2(int arr[10])
{
    for(int i=0; i<10; i++){
    	printf(" arr[%d] = %d\n", i, arr[i]);
    }
}

method_2(arr);

3.既然退化为指针!,可以直接使用指针形式传参,用指针进行接收,传的是数组首元素的地址,这里就必须单独添加一个参数表明待处理数组的元素个数

void method_3(int *arr, int len)
{
    for(int i=0; i<len; i++){
    	printf(" arr[%d] = %d\n", i, arr[i]);
    }
}

method_3(arr, 10);

对形式参数使用const:

在K&R C的年代,避免以上提到的误传递指针修改原始数据的唯一方法是提高警惕。ANSI C提供 了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。例如:

int sum(const int ar[], int n); /* 函数原型 */

int sum(const int ar[], int n) /* 函数定义 */
{
    int i;
    int total = 0;
    for( i = 0; i < n; i++)
        total += ar[i];
    
    return total;
}

以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似ar[i]++的表达式,编译器会捕获这个错误,并生成一条错误信息。

这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一 样。一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用 const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用 const。

🎈(二)指针传参

如果不是数组,是其它普通类型,需要修改原数据的值,我们需要使用指针参数:

int main(void) {
	int a = 1;
	int *p = &a;
    
	void setnum(int* c) {
		scanf("%d",c);  //这里a就是地址,不用再加&
	}
	//调用该函数:
	setnum(&a); 
    printf("%d",a);
    
    setnum(p);  //p的值也是&a
	printf("%d",a);
    return 0;
}
  1. 如果需要修改原数据的值,需要使用指针作为参数,同时,调用函数时的实参应该是需要修改的值的地址,要加上&

  2. 当我们使用指针作为参数,也可以单纯传递一个指针,毕竟指针就是地址,就不需要使用&,这两种情况本质是一样的

  3. 函数要处理数组必须知道何时开始、何时结束。一般用一个整数形参表明待处理数组的元素个数,但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。

    int sump(int * start, int * end);
    
    int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26,31, 20 };
    
    long answer;
    answer = sump(marbles, marbles + SIZE);
    

⛳三、函数指针

假设有一个指向 int类型变量的指针,该指针储存着这个int类型变量储存在内存位置的地址。 同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。

🎈1.声明函数指针

声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。

void ToUpper(char *); // 把字符串中的字符转换成大写字符

//ToUpper()函数的类型是“带char * 类型参数、返回类型是void的函数”。 下面声明了一个指针pf指向该函数类型:

void (*pf)(char *); // pf 是一个指向函数的指针

第1对圆括号把*和pf括起来,表明pf是一个指向函数的指针。因此,(*pf)是一个参数列表为(char *)、返回类型为void的函数。

创建函数指针最简单的方法把函数声明移过来,把函数名改成 (* 函数指针名),即先写出该函数的原型,后把函数名替换成(*pf)形式的表达式,

🎈2.用函数指针访问函数

既然可以用数据指针访问数据,也可以用函数指针访问函数。奇怪的是,有两种逻辑上不一致的语法可以这样做,

void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);

char mis[] = "Nina Metier";

pf = ToUpper;
(*pf)(mis); // 把ToUpper 作用于(语法1)

pf = ToLower;
pf(mis); // 把ToLower 作用于(语法2)
  • 第1种方法:由于pf指向ToUpper 函数,那么*pf就相当于ToUpper函数,所以表达式(*pf)(mis)和ToUpper(mis) 相同。

  • 第2种方法:由于函数名是指针,那么指针和函数名可以互换使用,所以pf(mis) 和ToUpper(mis)相同。从pf的赋值表达式语句就能看出ToUpper和pf是等价的。

    拓展:

    由于历史的原因,贝尔实验室的C和UNIX的开发者采用第1种形式,而 伯克利的UNIX推广者却采用第2种形式。K&R C不允许第2种形式。但是, 为了与现有代码兼容,ANSI C认为这两种形式(本例中是(*pf)(mis)和 pf(mis))等价。

🎈3.函数指针作为函数参数

作为函数的参数是数据指针最常见的用法之一,函数指针亦如此,告诉该函数要使用哪一个函数,例如:

void show(void (* fp)(char *), char * str);
  • 它声明了两个形参:fp和str。fp形参是一个函数指针,str是一个数据指针。

  • 可以这样调用函数:

    show(ToLower, mis); /* show()使用ToLower()函数:fp = ToLower */
    show(pf, mis); /* show()使用pf指向的函数: fp = pf */
    
  • 把带返回值的函数作为参数传递给另一个函数有两种不同的方法。

    function1(sqrt); /* 传递sqrt()函数的地址 */
    function2(sqrt(4.0)); /* 传递sqrt()函数的返回值 */
    

    第1条语句传递的是sqrt()函数的地址,假设function1()在其代码中会使用该函数。第2条语句先调用sqrt()函数,然后求值,并把返回值(该例中是 2.0)传递给function2()。

🚀指针与数组

⛳一、指针表示法和数组表示法

从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法

数组完全可以使用指针来访问, days[i] 和 *(days+i) 这两个表达式是等价的,无论 days 是数组名还是指针变量,这两个表达式都没问题。但是,只有当 days 是指针变量时,才能使用ar++这样的表达式。

指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一 些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要任务是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。

⛳二、指针和多维数组

在学习指针数组和数组指针之前,有必要了解指针和多维数组之间的关系,例如:

int zippo[4][2]; /* 内含int数组的数组 */
  1. 同样,数组名zippo是该数组首元素的地址。zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。

  2. 因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。 而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素 (一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。

  3. 给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo 和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。因此, zippo + 1和zippo[0] + 1的值不同。

  4. 解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的 []运算符,得到引用对象代表的值。

    • 因为zippo[0]是该数组首元素(zippo[0] [0])的地址,所以*(zippo[0])表示储存在zippo[0][0]上的值(即一个int类型的值)。
    • *zippo代表该数组首元素(zippo[0])的值,但是 zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以 *zippo就是&zippo[0][0]。
    • **zippo与 *&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之, zippo是地址的地址,必须解引用两次才能获得原始值。
  5. 特别注意,与 zippo[2][1]等价的指针表示法是*(*(zippo+2) + 1)。

在这里插入图片描述

如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。

⛳三、指针数组

存储指针的数组叫指针数组,之前已经讲过,用双引号括起来的内容被视为指向该字符串储存位置的指针,同时为了增加对此概念的熟悉,这里我们就也用字符串来讲解指针数组

定义:

 类型 *指针数组名[元素个数] 
 
 //例如:定义一个有两个元素的指针数组,每个元素都是一个int类型指针变量
int *qishou[2];

字符串数组:

#define SLEN 40
#define LIM 5

//mytalents数组是一个内含5个指针的数组,共占用40字节
const char *mytalents[LIM] = {
    "Adding numbers swiftly",
    "Multiplying accurately", "Stashing data",
    "Following instructions to the letter",
    "Understanding the C language"
};

//yourtalents是一个内含5个数组的数组,每个数组内含40个char类型的值,共占用200字节。
char yourtalents[LIM][SLEN] = {
    "Walking in a straight line",
    "Sleeping", "Watching television",
    "Mailing letters", "Reading email"
};
  1. 使用一个下标时都分别表示一个字符串,如mytalents[0]和 yourtalents[0];使用两个下标时都分别表示一个字符,例如 mytalents[1][2]表示 mytalents 数组中第 2 个指针所指向的字符串的第 3 个字符’l’, yourtalents[1][2]表示youttalentes数组的第2个字符串的第3个字符’e’。
  2. 虽然mytalents[0]和 yourtalents[0]都分别表示一个字符串,但mytalents和yourtalents的类型并不相同。mytalents中的指针指向初始化时所用的字符串常量的位置,这些字符串字面量被储存在静态内存中;而 yourtalents 中的数组则储存着字符串字面量的副本,所以每个字符串都被储存了两次。
  3. 如果要用数组表示一系列待显示的字符串,请使用指针数组,因为它比二维字符数组的效率高。

⛳四、数组指针

指向数组的指针叫做数组指针,

定义:

 类型 (*数组指针名)[数组元素个数] 
 
 //例如:定义一个指向三个成员的数组的指针
int (*p)[3];

使用圆括号,因为[]的优先级高于*,必须用个圆括号将*p单独括起来,*先与pz结合,因此声明的是一个指向数组(内含两个int类型的
值)的指针不然就变成了指针数组

#include <stdio.h>
int main(void)
{
    int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 },{ 5, 7 } };
    int(*pz)[2];
    pz = zippo;  //p = &zippo[0]两者是一样的
    
    printf(" pz = %p, pz + 1 = %p\n", pz, pz + 1);
    printf("pz[0] = %p, pz[0] + 1 = %p\n", pz[0], pz[0] + 1);
    printf(" *pz = %p, *pz + 1 = %p\n", *pz, *pz + 1);
    printf("pz[0][0] = %d\n", pz[0][0]);
    printf(" *pz[0] = %d\n", *pz[0]);
    printf(" **pz = %d\n", **pz);
    printf(" pz[2][1] = %d\n", pz[2][1]);
    printf("*(*(pz+2) + 1) = %d\n", *(*(pz + 2) + 1));
    
    printf("%d\n",(*(pz+1))[1]);
    printf("%d",*(*(pz + 1) + 1));
    return 0;
}

下面是该程序的输出:
pz = 0x0064fd38, pz + 1 = 0x0064fd40
pz[0] = 0x0064fd38, pz[0] + 1 = 0x0064fd3c
*pz = 0x0064fd38, *pz + 1 = 0x0064fd3c
pz[0][0] = 2
*pz[0] = 2
**pz = 2
pz[2][1] = 3
*(*(pz+2) + 1) = 3
    
8
8

虽然p是一个指针,不是数组名,但是也可以使用p[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:

  1. 数组法:

     zippo[m][n] == (*(p+m))[n];
    
  2. 指针法:

    zippo[m][n] == *(*(p + m) + n)
    

⛳五、数组和指针的联系

初始化字符数组来储存字符串和初始化指针来指向字符串有何区别 (“指向字符串”的意思是指向字符串的首字符)?

同样,C语言字符串也是通过字符数组来实现的,这里也正好用字符数组来总结以下指针和数组的相关联系:能使用指针表示数组名,也可以用数组名表示指针

讲解代码:

const char * pt1 = "Something is pointing at me.";  //共29个字符,注意还有一个'\0'
//等同于:
const char ar1[] = "Something is pointing at me.";

1.数组形式(ar1[])

  • 在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符’\0’)

  • 每个元素被初始化为字符串字面量对应的字符。

  • 通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区 (static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串常量,另一个是储存在ar1数组中的字符串。

  • 此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。

    在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似 ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操 作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左 值),不能用于常量。

*2.指针形式(pt1)

  • 指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。

  • 另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置, 并把字符串的地址储存在指针变量中。

    该变量最初指向该字符串的首字符, 但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符(o)。

3.const

字符串常量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针,即常量指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)

如果把一个字符串常量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。

总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。

4.区别

假设有下面两个声明:

 char heart[] = "I love Tillie!"; 
 const char *head = "I love Millie!";
  1. 首先,两者都可以使用数组表示法

    for (i = 0; i < 6; i++)
    	putchar(heart[i]);
    putchar('\n');
    for (i = 0; i < 6; i++)
    	putchar(head[i]);
    putchar('\n');
    
  2. 其次,两者都能进行指针加法操作(指针表示法):

    for (i = 0; i < 6; i++)
    	putchar(*(heart + i));
    putchar('\n');
    for (i = 0; i < 6; i++)
    	putchar(*(head + i));
    putchar('\n');
    
  3. 不能使用指针修改字符串

    char * p1 = "Klingon";
    p1[0] = 'F'; // ok?
    
    printf("Klingon");
    printf("%s",p1);
    

    编译器可以使用内存中的一个副本来表示所有完全相同的字符串常量。char *字符指针指向的数据存储在静态存储区,里面的值不允许修改,相当于const char *

    如果要修改,改成用非const数组初始化为字符串常量即可:char p1[] = “Klingon”;

    实际上在过去,一些编译器由于这方面的原因,其行为难以捉摸,而另一些编译器则导致程序异常中断。因此,建议在把指针初始化为字符串字面量时使用const限定符:

    const char * pl = "Klingon"; // 推荐用法
    

行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/506840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习实战29-AIGC项目:利用GPT-2(CPU环境)进行文本续写与生成歌词任务

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下深度学习实战29-AIGC项目&#xff1a;利用GPT-2(CPU环境)进行文本续写与生成歌词任务。在大家没有GPU算力的情况&#xff0c;大模型可能玩不动&#xff0c;推理速度慢&#xff0c;那么我们怎么才能跑去生成式的模型…

14 KVM虚拟机配置-配置虚拟设备(其它常用设备)

文章目录 14 KVM虚拟机配置-配置虚拟设备&#xff08;其它常用设备&#xff09;14.1 概述14.2 元素介绍14.3 配置示例 14 KVM虚拟机配置-配置虚拟设备&#xff08;其它常用设备&#xff09; 14.1 概述 除存储设备、网络设备外&#xff0c;XML配置文件中还需要指定一些其他外部…

Python+selenium,轻松搭建 Web 自动化测试框架

在程序员的世界中&#xff0c;一切重复性的工作&#xff0c;都应该通过程序自动执行。「自动化测试」就是一个最好的例子。 随着互联网应用开发周期越来越短&#xff0c;迭代速度越来越快&#xff0c;只会点点点&#xff0c;不懂开发的手工测试&#xff0c;已经无法满足如今的…

云渲染靠谱吗,使用云渲染会不会被盗作品?

云渲染靠谱吗、安全吗&#xff1f;如果使用 云渲染会不会被盗作品......Renderbus瑞云渲染作为一个正经的云渲染平台&#xff0c;也时不时会收到这类疑问&#xff0c;首先&#xff0c;瑞云渲染是肯定靠谱的,各位可以放心使用。另外小编也将在本篇教你如何辨别云渲染平台是否安全…

通达信W底形态选股公式,也称双底形态

W底形态&#xff0c;也称双底形态&#xff0c;是一种经典的技术分析形态&#xff0c;代表了跌势的逆转。看起来像字母 "W"&#xff0c;描述了一波下跌&#xff0c;反弹&#xff0c;再次下跌到与上一波下跌相同或相近的位置&#xff0c;最后是另一波反弹。W底形态两次…

【细读Spring Boot源码】@ComponentScan是如何生效的?

前言 在使用SpringBoot使用过程中 RestController、Service、Repository这几个注解类上都标有Component注解 启动类上标有的SpringBootApplication注解类上有个ComponentScan注解。那么ComponentScan如何把相关的对象注册到BeanFactory的&#xff1f; 找到处理ComponentScan注…

【Qt 从入门到入土】下篇

【Qt 从入门到入土】上篇 一个非常好的学习 Qt 的视频 本文目录 6. 对话框QDialog6.1 基本概念6.2 标准对话框6.3 自定义消息框6.4 消息对话框6.5 标准文件对话框 7. 布局管理器7.1 系统提供的布局控件7.2 利用widget做布局 8. 常用控件8.1 QLabel 控件使用8.2 QLineEdit8.3 其…

1_5 pytorch操作

一、torch 算子 1、torch.nn.functional.affine_grid(theta, size) 给定一组仿射矩阵(theta)&#xff0c;生成一个2d的采样位置(流场)&#xff0c;通常与 grid_sample() 结合使用,用于空间仿射变换网络&#xff0c;用于对2D或3D数据进行仿射变换。 输入&#xff1a;theta(Te…

5.20 牛奶咖啡·仙羽「重生」巡回演唱会 武汉站 告白夜浪漫收官

牛奶咖啡主唱仙羽「重生」主题巡回演唱会将于2023年5月20日浪漫收官。经历多次延期&#xff0c;「重生」巡回演唱会武汉站 终于与大家见面&#xff0c;届时会为大家带来多首传唱度极高的歌曲&#xff0c;与歌迷朋友共度一个难忘的告白夜。 关于仙羽仙羽&#xff08;kiki&#…

微服务架构演变

微服务架构演变 认识微服务 服务架构演变 单体架构&#xff1a;将业务的所有功能集中在一个项目种开发&#xff0c;打成一个包部署 优点&#xff1a; 架构简单部署成本低 缺点&#xff1a; 耦合度高 分布式架构&#xff1a;根据业务功能对系统进行拆分&#xff0c;每个业…

python4delphi之初体验

最近需要做一个excel导入的工具, excel表格是python的强项,于是想delphi结合python实现 delphi环境xe2 python 3.6.6 上图可以看到,真的很爽,我在vscode写python脚本,然后给delphi调用 读取了一个excel文件,打印出了列头。 最爽的是,我在没wps,office环境的机…

【计算机视觉 | 目标检测】Grounding DINO 深度学习环境的配置(含案例)

“Grounding DINO&#xff1a;Marrying DINO with Grounded Pre-Training for Open-Set Object Detection”的官方 PyTorch 实现&#xff1a;SoTA 开放集对象检测器。 文章目录 一、Helpful Tutorial二、相关的论文工作2.1 相关的论文整理2.2 论文的亮点2.3 论文介绍2.4 Marryi…

shell脚本之数组与冒泡排序

目录 一. 数组1.1 数组定义&#xff1a;1.2 数组包括的数据类型&#xff1a;1.3 向函数传入数组的值 二. 冒泡排序算法 一. 数组 1.1 数组定义&#xff1a; 方法一&#xff1a; 数组名&#xff08; 1 2 3 4 5 &#xff09; 方法二&#xff1a; 数组名&#xff08; [0]1 [1]2 […

Nginx 概述和缓存能力

官方说明 理解说明 Nginx是一个高性能的Web服务器和反向代理服务器&#xff0c;其设计思想和执行机制可以概括如下&#xff1a; 异步事件驱动&#xff1a;Nginx采用基于事件驱动的异步I/O模型&#xff0c;这意味着它能够处理大量并发连接而不会消耗太多的系统资源。 单线程模…

(六)Kubernetes - 手动部署(二进制方式安装)

Kubernetes - 手动部署 [ 5 ] 1 部署NginxKeepalived高可用负载均衡器1.1 安装软件包(Master1/Master2)1.2 Nginx配置文件(主备相同)1.3 keepalived配置文件(Master1)1.4 keepalived配置(master2)1.5 Nginx增加Steam模块1.5.1 查看Nginx版本模块1.5.2 下载同一个版本的nginx1.5…

如何提高执行力

在最近的工作中很多事情容易拖延的&#xff0c;导致部分工作和自己的生活容易因为拖延导致混乱&#xff0c;比如没有提前预订好抢票&#xff0c;结果导致放假买不到票等等的事情出现。 所以很多事懒的原因是执行力差&#xff0c;为什么执行力差&#xff0c;说明事情不重要。那就…

DNS欺骗、ARP攻击及钓鱼网站制作

数据来源 本文仅用于信息安全的学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 一、背景 钓鱼者运用社会工程学( social engineering)知识诱骗受害者&#xff0c;以在…

buu [AFCTF2018]花开藏宝地 1

题目描述&#xff1a; 题面&#xff1a; 第80804238007977405688648566160504278593148666302626415149704905628622876270862865768337953835725801963142685182510812938072115996355782396318303927020705623120652014080032809421180400984242061592520733710243483947230…

Yolov8改进---注意力机制: SimAM(无参Attention)和NAM(基于标准化的注意力模块),效果秒杀CBAM、SE

🏆🏆🏆🏆🏆🏆Yolov8魔术师🏆🏆🏆🏆🏆🏆 ✨✨✨魔改网络、复现前沿论文,组合优化创新 🚀🚀🚀小目标、遮挡物、难样本性能提升 🍉🍉🍉定期更新不同数据集涨点情况 1. SimAM:无参Attention 论文: http://proceedings.mlr.press/v139/yang…

瑞云渲染农场怎么用,瑞云渲染多少钱一分钟?

Renderbus瑞云渲染农场作为亚洲前沿的 云渲染平台&#xff0c;一直以“做最好的云渲染工具”为愿景&#xff0c;紧跟CG行业的技术创新与发展&#xff0c;致力于提供专业可靠、安全稳定、可持续创新的云渲染解决方案&#xff0c;助力推动行业快速发展&#xff0c;被誉为中国云渲…