C/C++语言笔记

本文详细探讨了C/C++编程中的关键概念,包括循环嵌套优化、引用特性、数组与指针的区别、指针操作、库函数使用、变量声明(如extern、volatile和register)、转义字符、位段操作、内存管理以及C++特性的深入理解,如虚基类、虚函数、纯虚函数和抽象类等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

循环嵌套

在多层循环中,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。

如果循环体内存在逻辑判断,并且循环次数很大,应将逻辑判断移到循环体外。


引用

不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)

int &m=NULL;    //error

不能建立指针的引用  

int * &n = &m;    //error

数组与指针

        指向字符串的指针不能修改字符串(常量字符串位于静态储存区,不可修改!)。数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

char a[]="hello";
a[0] = 'x';            //正确
char *p = "world";     //p指向常量储存区    可理解为 const char *p = "world"
p[0] = 'x';            //错误,编译器不能发现该错误

        当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针

void Func(char a[100])
{
cout<< sizeof(a) << endl;    //4 字节而不是100字节
}

指针

        如果 p 是 NULL 指针,那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p连续操作两次就会导致程序运行错误。

*(ptr++) += 100;        <-- 指针移一次
    *(ptr++) = *(ptr++) + 100;    <-- 指针移两次


库函数

1.整数转字符串:sprintf()

/*----------------------------------------------------------------------------

* 函数名:int sprintf( char *buffer, const char *format, [ argument] … );
* 功  能:把格式化的数据写入某个字符串缓冲区。
* 参  数:buffer:char型指针,指向将要写入的字符串的缓冲区。
		 format:格式化字符串。
		 [argument]...:可选参数,可以是任何类型的数据
* 返回值:返回写入buffer 的字符数,出错则返回-1. 如果 buffer 或 format 是空指
         针,且不出错而继续,函数将返回-1,并且 errno 会被设置为 EINVAL
----------------------------------------------------------------------------*/

举例:

int n=123;
char str[4];
sprintf(str,"%d",n);
puts(str);    //str*="123"

2.atof()函数在 <stdlib.h> 中


变量

extern

C语言中不能将变量放在.h文件中重复调用 可用extern

volatile

立即读取内存变量(禁止编译器优化)

volatile 影响编译器编译的结果,指出 volatile 变量每次使用时都需要去内存里重新读取它的值,而一旦修改此变量就直接写入内存,而不是操作cache中的变量。 volatile 变量有关的运算,不要进行特殊优化。

{ 
	int I = 10; 
	int j = I; 
	int k=I; 
}

对于上面的代码段,优化的作法是,编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会从 cache 中把上次读的数据 i 放在 k 中。而不是重 新从 i 的内存里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问,不会出错。

另一个例子:

[task1.c]
int volatile i; 

while(i) 
{ 
    do_something(); 
} 

[task2.c]
do_something(i);

         如果 i 没有被 volatie 修饰,当 while 循环执行时,另一段程序并发的执行了 i=0 ,这个循环仍不会退出,因为每次循环都是检查cache中的值。 如果有 volatie 修饰,那么循环结束,因为循环每次检查 i 的时候,会先从内存把 i 读入寄存器,这个时候 i 在其它地方被赋 0 ,则循环结束。

        对于程序中存在多个执行流程访问同一全局变量的情况, volatile 限定符是必要的。

register

        在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。因此,C\C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。

        寄存器是与机器硬件密切相关的,不同类型的计算机,寄存器的数目是不一样的,通常为2到3个,对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。


转义字符 \t

\t代表是一个tab键值,就是8位,具体空格的个数,跟你的数值有关系,比如你的字符串是abc,加个\t,则空格是5个,如果字符串是abcde,加\t,则空格是3个


位段(bit field)

该种形式出现于结构体或共用体的定义中,是位域定义的标准形式。其使用方式为:

struct name
{
    type var_name : n;
};

含义为,在结构体name汇总,成员变量var_name占用空间为n位。n为正整数,其值必须小于type类型占用的位数。比如type如果是int,占4字节32位,那么n必须是1~31之间的整数。

对于位域类型的成员,在赋值时如果实际值超过n位所能表达的范围,那么超出部分将会被截掉,只保存低位值。如int var:4,本身只有4位的空间,如果赋值var = 20, 由于20的二进制值为10100,实际为五位,这时var实际被赋值的就是低四位,0100,即4。

由于C语言中的地址是针对字节计算的,所以位域类型的成员变量不支持取地址操作,即对于结构体a, 如果存在位域成员变量var,那么&a.var是非法的,编译会出错。


__packed

__packed是字节对齐的意思,比如说int float double char它的总大小是4 + 4 + 8 + 1 = 17。但如果不用__packed的话,系统将以默认的方式对齐(假设是4字节),那么它占4 + 4 + 8 + 4 = 20;(不足4字节以4字节补齐)。

注:在VS2008上述方法不能用了,如果想设置对齐方式,只要选择工程属性-配置属性-C/C++-代码生成就能设置,它的选项有1、2、4、8、16。在GCC下,可以在加上#pragma pack(4) 4字节对齐的意思,其它同理。


数据合并

u8 a[2] = {0x10,0x20};
u16 b;
b = a[1]<<8 + a[0];		//错误,必须加括号
b = (a[1]<<8) + a[0];	//正确(隐式类型转换)
b = ((u16)a[1]<<8) + (u16)a[0];		//正确(显式类型转换)
b = ((u16)a[1]<<8) | (u16)a[0];		//正确

数据拆分

u16 b = 0x1020;
u8 a[2];

a[0] = b/256;		//a[0] = 0x10
a[1] = b%256;		//a[1] = 0x20

a[0] = b/0xFF;	
a[1] = b%0xFF;	

a[0] = b>>8;		//隐式转换
a[1] = b;		

a[0] = u8(b>>8);	//显式转换
a[1] = (u8)b;	

C++几个概念

  • 不同类型成员的访问权限

public:        内、外、派

protected:  内、派

private:      内

  • 初始化列表

构造函数除了有参数列表之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。

class X
{
private:
    int id;
    int age;
    int other;

public:
    X(int i, int a, int o): id(i), age(a)    //初始化列表
    {
        other = o;    //也可以使用传统初始化方法
    }
};
  • const 成员变量

即为 const 修饰的成员变量,其只能在构造函数中通过初始化列表来进行初始化,数据为 “只读”,不会再程序运行中发生改变(不允许赋值操作)。

  • const 成员函数

指 const 放在成员函数的后面,表明这个函数不会修改类中任何数据,也不能在 const 的成员函数中,调用其他非 const 的成员函数。

  • 静态数据成员

即数据成员的定义前面增加了 static 关键字,其只能在外部程序开始执行前初始化一次(类似于全局变量那样创建),然后被所有成员共享,任一对象都可修改该共享数据。主要用于各个对象的公用数据,如统计总数(如下代码中的 i )、各种文件的共用路径等(如下代码中的 path 和 p)。静态数据成员独立于所有对象,甚至在对象未被声明时已经存在(编译时已被创建并初始化,默认为0),在main函数(程序)结束前,其内存都不会销毁。

class Student{
public:
    //不能在此处初始化,声明类时不分配内存 
    static int count;	
    static char univ[]; 
    static const char *addr; 
	
    Student() { count++; }	//每定义一个学生,人数自动加一 
};
//一般在类之后,main之前进行定义和初始化(带上数据类型)
int Student::count = 0;    
char Student::univ[20] = "MIT";		
const char * Student::addr = "Boston";

int main()
{
    Student::count = 10;    		//在声明对象之前已经存在
//  base::path[20] = "/home/alise";	//错误 
    strcpy(Student::univ, "Harvard"); 	//原来的字符串已被覆盖
    memcpy(Student::univ, "Stanford", 9);
    Student::addr = "San Francisco";	//正确,p指向了另一个字符串 
    
    Student jack, tom;		 //创建两个对象后count变为12 
    cout<< jack.count <<endl;    //输出 12
    cout<< tom.count <<endl;     //输出 12
    
    jack.count = 2;	         //所有对象的 count 都会改变 
    tom.addr = "xian"; 
    
    cout<< jack.count <<endl;    //输出 2
    cout<< tom.count <<endl;     //输出 2
    cout<< jack.univ <<endl;	 //输出 "Stanford"
    cout<< jack.addr <<endl;	 //输出 "xian"
    cout<< tom.addr <<endl;	 //输出 "xian"
    
    return 0;
}

【引申】:数组、指针与字符串

对数组初始化字符串时,数组仅表示这个字符串的首地址(不是变量),故其不能被赋值(只能在初始化时定义一次,即定义了表示的内存地址内存大小),数组只能表示该字符串的首地址。其字符串分配在 “栈区”,故可以通过数组修改字符串,想要一次全部修改时,可通过 strcpy() 或memcpy() 函数。

对指针初始化字符串时,指针变量储存在 “栈区”,而字符串储存在 “常量储存区”,故不可以通过指针修改字符串(所以一般定义时要加上 const 关键字修饰),但指针可以重新赋值(重新指向另一个字符串)。

  • 静态成员函数

即成员函数前面加 static,其只能访问静态数据成员,不能访问非静态数据成员,也不能访问非静态成员函数。

class Student{
private:
    static int count;	
public:    
    static void set(int n) { count = n; }
//  static void set(int count) {count = count; }    //错误
    static void show(void) { cout<< count <<endl; }
};

int Student::count = 0;    //可外部初始化

int main()
{
//  Student::count = 1;     //错误,私有数据成员,不能直接调用
    Student::set(1);        //通过类名调用静态成员函数
	Student::show();
	
    Student tom;
    tom.show();    //输出 1
    tom.set(2);	 
    tom.show();    //输出 2
   
    return 0;
}

【注意】:静态成员函数的形参不能与静态数据成员同名,否则参数传递会失效。

  • 友元

指在函数或类的前面加 friend 关键字,包括:一般友元函数、友元成员函数、友元类。其可允许在类的外部访问类的私有成员

:: 运算符

:: 是运算符中等级最高的,它分为三种:

(1) global scope(全局作用域符),用法(::name)

int a;
int main()
{
    int a;

    a=1;      // 局部变量a
    ::a=2;    // 全局变量a
}

当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分

(2)类的外部定义成员函数

(3)调用静态数据成员和静态成员函数

(4) class scope(类作用域符),用法(class::name)

例如:C继承A,B,而A,B中皆有成员 成员函数f(),则当从C中调用 f 时会产生二义性

C.f();       // 二义性错误
C.A::f();    // 正确,调用基类A中的f()
C.B::f();    // 正确,调用基类B中的f()

(5)引用类中 typedef 的新类型

C++引入了 “仅在类内部起作用的类型别名(并且还有派生方式之分)” ,通过限制该类型别名的作用域来防止冲突。

比如同样表示长度,可能有的类中只须char即可,有的类中要用int,而有的类可能连long都嫌小。

class A{
private:
    typedef char size_c;
public:    
    typedef int size_mm;
	
    size_mm length;
    size_c *str;
    
    size_mm set(size_mm l) 
    { 
        length = l; 
        return length; 
    }
};
class B{
public:    
    typedef int size_cm;
	
    size_cm length;
};

int main()
{
//  A::size_c *path;	//错误,私有类型不能在外部引用 
    A::size_mm a;	//定义类A的size_mm类型变量 

    return 0;
}

(6) namespace scope(命名空间作用域符),用法(namespace::name)

C++中的::的作用

c++的类中typedef的作用

虚基类

为了解决“二义性”的问题

假设有基类A,如果直接让类 B,C 继承 A,而 D 继承 B 和 C,即一种 “菱形” 结构,当D访问A中的成员时,则会产生 “二义性” 的问题,不知是通过 B 这条路的 A 还是 C 这条路径的A。

故可以将B和C定义为虚继承,此时B和C共享A,当D访问A中的成员时就没有了“二义性”(只有一个A)。

C++虚继承和虚基类详解

派生对象指针

  • 指向基类对象的指针可以指向其公有派生的对象,且只能访问派生对象中从基类继承而来的成员。
  • 指向派生对象的指针不能指向其基类对象

虚函数

虚函数允许函数调用在运行时才建立,

如:当A中有成员函数 f(),B继承A,其也有成员函数 f(),当定义基类A的指针p后,再令其指向派生B的对象,调用 p->f(),此时调用的函数为A中的 f()。

这时,将基类A中的成员函数 f() 声明为虚函数(B中的 f() 可以不声明为虚函数),同上调用 p->f() 后,调用的将是B中的 f()。

即,虚函数机制允许指向基类的指针,指谁,运行谁的(同名)成员函数

纯虚函数 / 抽象类

纯虚函数:在基类中没有定义,但要求在派生类中定义自己的版本。

抽象类:至少包含一个纯虚函数的类。

如:计算三角形和矩形的面积,两者都可以提炼出 “形状” 这个基类,其包含长,高和计算面积的纯虚函数(仅声明,没有定义),而在派生类 “三角形” 和 “矩形” 则继承 “形状” 类,并分别在其中定义具体的计算面积函数。

参考:俄罗斯方块代码(链接)

内存分配

C/C++语言中分了如下几个内存区域

1. 栈区

是最常见的内存区域,由编译器自动分配并在函数结束时释放,如函数的形参、实参和局部变量。运行效率高,但容量有限。其缓存方式为 “后进先出”,使用的是一级缓存。

【注】:在Windows下,栈是向低地址扩展的数据结构(Linux根据版本,情况不同),是一块连续的内存的区域,即栈顶的地址和栈的最大容量是系统预先规定好的(在VC6下面,默认的栈空间大小是1M,多了会溢出,运行出错)

2. 堆区

分配(malloc() / new)和释放(free() / delete)皆由程序员管理(程序全部执行完后也会由OS释放),可在运行时任意调整大小(故也称为“动态内存分配”)可使用的容量也及较大,使用的是二级缓存。

【注】:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

3. 静态储存区

内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量和 static 变量。

4. 常量区

存放常量,不可修改,如宏定义、代码中的数字、常量字符串、C++中 const 修饰的变量。

【注】:C中 const 修饰的变量仍在 “栈” 中。

5. 程序代码区

存放程序文件,运行后不可修改

一个例子:

// 整个程序文件在程序代码区 
#include<iostream>

#define PI 3.14		// 常量区 
int a = PI;			// PI在常量区,a在静态储存区 

int main()
{
	int b = 2;		// 2在常量区,b在栈区 
	char s[] = "123abc";	// 数组s在栈中,内容为"123abc" 
	char *p1 = "456def";    // 指针 p1 在栈中,指向的字符串"456def"在常量区
	int *p2 = (int *)malloc(sizeof(int)*10);    // 指针 p2 在栈中,指向的内存在堆中
	static int d = 3;	// 静态储存区 
	
	return 0;
} 

C++内存管理(超长,例子很详细,排版很好)

字符串常量到底存放在哪个存储区

全局/静态存储区、常量存储区

堆、栈、自由存储区、全局/静态存储区、常量存储区比较

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值