C++ 拾遗 2

1.变量的作用域

1)全局变量

在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问。

全局变量在主程序退出时由系统收回内存空间。

2)局部变量

函数或语句内部的语句使用在函数或语句块外部是不可用的

局部变量在函数返回或语句块结束时由系统收回内存空间。

3)静态局部变量

static修饰的部变量生命周期和程序相同,并且会被初始化一次

作用域为局部,当定义它的函数或语句块结束时,其作用域随之结束。

当程序想要使用全局变量的时候应该先考虑使用static考虑到数据安全性

4)注意事项

  1. 全局变量和静态局部变量自动初始化为0。
  2. 局部变量不会自动初始化,其值是不确定的,程序中应该有初始化局部变量的代码,否则编译可能会报错(不同的编译器不一样)。
  3. 局部变量和全局变量的名称可以相同,在函数或语句块,如果局部变量名与全局变量名相同,就屏蔽全局变量而使用局部变量,如果想使用全局变量,可以在变量名前加两个冒号(::)。
  4. for循环初始化语句中定义的变量的作用域是for语句块。

2.函数分文件编写

头文件(*.h:需要包含的头文件,声明全局变量,函数的声明,数据结构和类的声明等。

源文件(*.cpp:函数的定义、类的定义。

3.字符串

C++中的字符串操作可以分为两种主要风格:

C风格字符串和C++风格字符串。以下是它们的特点和常用操作,并附带相应的示例代码:

C风格字符串:

  • C风格字符串的本质是字符数组。
  • 字符数组以null终止,即以\0表示字符串的结束。
  • 在C风格字符串中,你需要手动分配内存,并负责管理字符串的长度和内存。

C++风格字符串:

  • C++风格字符串的本质是std::string类,它封装了字符串操作。
  • std::string自动管理内存,无需手动分配或释放内存
  • 支持丰富的字符串操作,提供更高的安全性和便捷性。

C++风格字符串的常用操作:

  • 赋值:std::string str = "字符串内容";
  • 拼接:str = str + "更多字符串内容";
  • 追加:str += "追加的字符串";
  • 长度:int length = str.length();
  • 比较:if (str1 == str2) { /* 相等 */ } else { /* 不相等 */ }
  • 截取子串:std::string subStr = str.substr(startIndex, length);
  • 查找子串:size_t found = str.find("目标子串");
#include 
#include 

int main() {
    // 赋值操作
    std::string str1 = "Hello, ";
    std::string str2 = "world!";
    
    // 拼接操作
    std::string result = str1 + str2;
    std::cout << "拼接后的字符串: " << result << std::endl;
    
    // 追加操作
    result += " Welcome to C++!";
    std::cout << "追加后的字符串: " << result << std::endl;
    
    // 字符串长度
    std::cout << "字符串长度: " << result.length() << std::endl;
    
    // 字符串比较
    std::string compareStr1 = "Hello, world!";
    std::string compareStr2 = "Welcome to C++!";
    
    if (compareStr1 == compareStr2) {
        std::cout << "字符串相等" << std::endl;
    } else {
        std::cout << "字符串不相等" << std::endl;
    }
    
    // 截取子串
    std::string subStr = result.substr(7, 5); // 从位置7开始截取5个字符
    std::cout << "截取的子串: " << subStr << std::endl;
    
    // 查找子串
    size_t found = result.find("C++"); // 查找C++在字符串中的位置
    if (found != std::string::npos) {
        std::cout << "子串 \"C++\" 找到,位置:" << found << std::endl;
    } else {
        std::cout << "子串 \"C++\" 未找到" << std::endl;
    }
    
    return 0;
}

C风格

C语言约定:如果字符型(char数组末尾包含空字符\0(也就是0)那么该数组中的内容就是一个字符串

memset(name,0,sizeof(name));   // 把全部的元素置为0。
name[0]=0;       // 不规范,有隐患,不推荐。

char *strcpy(char* dest, const char* src);
功 能: 将参数src字符串拷贝至参数dest所指的地址。
返回值: 返回参数dest的字符串起始地址。
复制完字符串后,会在dest后追加0。
如果参数dest所指的内存空间不够大,会导致数组的越界。

char * strncpy(char* dest,const char* src, const size_t n);
功能:把src前n个字符的内容复制到dest中。
返回值:dest字符串起始地址。
如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。
如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0。
如果参数dest所指的内存空间不够大,会导致数组的越界。

size_t  strlen( const char*  str);
功能:计算字符串的有效长度,不包含0。
返回值:返回字符串的字符数。
strlen()函数计算的是字符串的实际长度,遇到0结束。

字符串拼接strcat()
char *strcat(char* dest,const char* src);
功能:将src字符串拼接到dest所指的字符串尾部。
返回值:返回dest字符串起始地址。
dest最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个0。
如果参数dest所指的内存空间不够大,会导致数组的越界。

字符串比较strcmp()和strncmp()
int strcmp(const char *str1, const char *str2 );
功能:比较str1和str2的大小。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
int strncmp(const char *str1,const char *str2 ,const size_t n);
功能:比较str1和str2前n个字符的大小。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。
在实际开发中,程序员一般只关心字符串是否相等,不关心哪个字符串更大或更小。

查找字符strchr()和strrchr()
const char *strchr(const char *s, int c);
返回在字符串s中第一次出现c的位置,如果找不到,返回0。
const char *strrchr(const char *s, int c);
返回在字符串s中最后一次出现c的位置,如果找不到,返回0。

查找字符串strstr()
char *strstr(const char* str,const char* substr);
功能:检索子串在字符串中首次出现的位置。
返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。

4.指针

1)指针变量

指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中起始地址

语法数据类型 *变量名;

数据类型必须是合法的C++数据类型(intchar、double或其它自定义的数据类型)。

星号*与乘法中使用的星号是相同的但是,在这个场景中,星号用于表示这个变量是指针。

2)对指针赋值

不管是整型、浮点型、字符型,还是其的数据类型的变量它的地址都是一个十六进制数我们用整型指针存放整数型变量的地址;用字符型指针存放字符型变量的地址;用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址

语法指针=&变量名;

注意

  1. 对指针的赋值操作也通俗的被称为“指向某变量”,被指向的变量的数据类型称为“基类型”。
  2. 如果指针的数据类型与基类型不符,编译会出现警告。但是,可以强制转换它们的类型。

3)指针占用的内存

指针也是变量,是变量就要占用内存空间。

在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节。

在C++中,指针是复合数据类型,复合数据类型是指基于其它类型而定义的数据类型,在程序中,int是整型类型,int*是整型指针类型,int*可以用于声明变量,可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int*当成一种数据类型就是了。

4)使用指针

声明指针变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。

指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)

*运算符被称为间接值解除引用(解引用)运算符,将它用于指针,可以得到该地址的内存中存储的值,*也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。

5)指针用于函数的参数

如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递传地址

值传递:函数的形参是普通变量。

传地址的意义如下:

  1. 可以在函数中修改实参的值。
  2. 减少内存拷贝,提升性能。

6)const 修饰指针

语法const 数据类型 *变量名;(左定值)

不能通过解引用的方法修改内存地址中值(用原始的变量名是可以修改的)。

注意:

  1. 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
  2. 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值
  3. 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义。

如果形参的值不需要改变,建议加上const修饰,程序可读性更好。

语法数据类型 * const 变量名;

指向的变量(对象)不可改变。

注意:

  1. 在定义的同时必须初始化,否则没有意义。
  2. 可以通过解引用的方法修改内存地址中值。

C++编译器把指针常量做了一些特别的处理,改头换面之后,有一个新的名字,叫引用。

7)空指针

如果对空指针解引用,程序会崩溃。

如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。

在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。

为什么空指针访问会出现异常?

NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。

8)野指针

野指针就是指针指向的不是一个有效(合法)的地址。

在程序中,如果访问野指针,可能会造成程序的崩溃。

出现野指针的情况主要有三种:

1)指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)。

2)如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效。

3)指针指向的变量已超越变量作用域(变量的内存空间已被系统回收),让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针

规避方法:

1)指针在定义的时候,如果没地方指,就初始化为nullptr。

2)动态分配的内存被释放将其置为nullptr

3)函数不要返回局部变量的地址。

注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序的崩溃。是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。

4.void关键字

在C++中,void表示无类型,主要有三个用途:

1)函数的返回值用void,表示函数没有返回值。

void func(int a,int b)

{

    // 函数体代码。

    return;

}

2)函数的参数填void,表示函数不需要参数(或者让参数列表空着)。

int func(void)

{

    // 函数体代码。

    return 0;

}

3)的形参用void *,表示接受任意数据类型的指针。

注意:

  1. 不能用void声明变量,它不能代表一个真实的变量,但是,用void *可以。
  2. 不能对void *指针直接解引用(需要转换成其它类型的指针)。
  3. 把其它类型的指针赋值给void*指针不需要转换。
  4. 把void *指针赋值给把其它类型的指针需要转换。

5.动态内存分配

使用堆区的内存有四个步骤:

1)声明一个指针

2)用new运算符向系统申请一块内存,让指针指向这块内存

3)通过对指针解引用的方法,像使用变量一样使用这块内存

4)如果这块内存不用了,用delete运算符释放它

申请内存的语法:new 数据类型(初始值);   // C++11支持{}

如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)。

释放内存的语法:delete 地址;

释放内存不会失败(还钱不会失败)。

  1. 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
  2. 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。
  3. 动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收。
  4. 就算指针的作用域已失效,所指向的内存也不会释放。
  5. 用指针跟踪已分配的内存时,不能跟丢。

#include          // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	// 1)声明一个指针;
	// 2)用new运算符向系统申请一块内存,让指针指向这块内存;
	// 3)通过对指针解引用的方法,像使用变量一样使用这块内存;
	// 4)如果这块内存不用了,用delete运算符释放它。
	// 申请内存的语法:new 数据类型(初始值);   // C++11支持{}
	// 释放内存的语法:delete 地址;
	int* p = new int(5);
	cout << "*p=" << *p << endl;
	*p = 8;
	cout << "*p=" << *p << endl;
	delete p;

	/*	for (int ii = 1; ii > 0; ii++)
	{
		int* p = new int[100000];     // 一次申请100000个整数,这个语法以后再讲。
		cout << "ii="<

6.一维指针和数组

1)指针的算术

将一个整型变量加1后,其值将增加1。

但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数

2)数组的地址

a)数组在内存中占用的空间是连续的。

b)C++将数组名解释为数组第0个元素的地址。

c)数组第0个元素的地址和数组首地址的取值是相同的。

d)数组第n个元素的地址是:数组首地址+n

e)C++编译器把   数组名[下标]  解释为  *(数组首地址+下标)

3)数组的本质

数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。

4)数组名不一定会被解释为地址

在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。

可以修改指针的值,但数组名是常量,不可修改。

5)用new动态创建

普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。

动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];

释放一维数组的语法:delete [] 指针;

注意:

  1. 动态创建的数组没有数组名,不能用sizeof运算符。
  2. 可以用数组表示法和指针表示法两种方式使用动态创建的数组。
  3. 必须使用delete[]来释放动态数组的内存(不能只用delete)。
  4. 不要用delete[]来释放不是new[]分配的内存。
  5. 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。
  6. 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。
  7. 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
  8. 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
  9. 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存。

6)数组排序

#include          // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int compasc(const void* p1, const void* p2)         // 升序的回调函数。
{
	return *((int*)p1) - *((int*)p2);
}

int compdesc(const void* p1, const void* p2)       // 降序的回调函数。
{
	return *((int*)p2) - *((int*)p1);
}

int main()
{
	int a[8] = { 4,2,7,5,8,6,1,3 };

	qsort(a,sizeof(a)/sizeof(int),sizeof(int),compasc);                   // 对数组a进行升序排序。

	for (int ii = 0; ii < 8; ii++)
	{
		cout << "a[" << ii << "]=" << a[ii] << endl;
	}

	qsort(a, sizeof(a) / sizeof(int), sizeof(int), compdesc);            // 对数组a进行降序排序。

	for (int ii = 0; ii < 8; ii++)
	{
		cout << "a[" << ii << "]=" << a[ii] << endl;
	}
}

7.共同体

共同体(共用体、联合体)是一种数据格式,它能存储不同的数据类型,但是,在同一时间只能存储其中的一种类型。

声明共同体的语法:

union 共同体名

{

成员一的数据类型  成员名一;

成员二的数据类型  成员名二;

成员三的数据类型  成员名三;

......

成员n的数据类型  成员名n;

};

注意:

  1. 共同体占用内存的大小是它最大的成员占用内存的大小(内存对齐)。
  2. 全部的成员使用同一块内存。
  3. 共同体中的值为最后被赋值的那个成员的值。
  4. 匿名共同体没有名字,可以在定义的时候创建匿名共同体变量(VS和Linux有差别),也可以嵌入结构体中。

应用场景:

  1. 当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间(嵌入式系统)。
  2. 用于回调函数的参数(相当于支持多种数据类型)。

8.引用

引用形参和const

如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量。

什么时候将创建临时变量呢?

  1. 引用是const
  2. 数据对象的类型是正确的,但不是左值。
  3. 数据对象的类型不正确,但可以转换为正确的类型。

结论:如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。

将引用形参声明为const的理由有三个:

  1. 使用const可以避免无意中修改数据的编程错误。
  2. 使用const使函数能够处理const和非const实参,否则将只能接受非const实参。
  3. 使用const,函数能正确生成并使用临时变量。

左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针。

非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式。

引用用于函数的返回值

传统的函数返回机制与值传递类似。

函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值。

double m=sqrt(36);      // sqrt()是求平方根函数。

sqrt(36)的返回值6被拷贝到临时的位置,然后赋值给m。

cout << sqrt(25);

sqrt(25)的返回值5被拷贝到临时的位置,然后传递给cout

如果返回的是一个结构体,将把整个结构体拷贝到临时的位置。

如果返回引用不会拷贝内存。

语法:

返回值的数据类型& 函数名(形参列表);

注意:

  1. 如果返回局部变量的引用,其本质是野指针,后果不可预知。
  2. 可以返回函数的引用形、类的成员、全局变量静态变量
  3. 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型。

9.各种形参的使用

1)如果不需要在函数中修改实参

  1. 如果实参很小,如C++内置的数据类型或小型结构体,则按值传递。
  2. 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)。
  3. 如果实参是较大的结构,则使用const指针或const引用。
  4. 如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。

2)如果需要在函数中修改实参

  1. 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x。
  2. 如果实参是数组,则只能使用指针。
  3. 如果实参是结构体,则使用指针或引用。
  4. 如果实参是类,则使用引用。

你可能感兴趣的:(程序设计语言基础,c++,java,jvm)