C, PHP, VB, .NET

Дневникът на Филип Петров


* Указатели, Адресна аритметика

Публикувано на 11 октомври 2008 в раздел С/С++.

Всеки обект в езика (променлива, константа, масив, елемент на масив, структура, …) се съхранява в определени клетки на паметта. Прието е да се казва, че тези клетки се намират на определен „адрес“. Променлива, чиято стойност е адрес в паметта, се нарича указател. Стойността на указателя посочва местоположението на дадена променлива и позволява косвен достъп до стойността й.

Указателите се дефинират също както обикновените променливи, но в началото на името се добавя знака „*“. Например:

	// Дефинира указател към променлива от тип int
	int *p;

Важно условие, е че типа данни в дефиницията на указателя трябва да съотвества на типа данни на променливата, към която ще сочи. Адреса на дадена променлива може да се изведе чрез знака „&“. Ето и пример:

	// Две промениви от тип int
	int x = 5, y;
	// Два указателя
	int *px, *py;
	// На указателя px присвояваме адреса на променливата x
	px = &x;
	// Чрез *px извличаме стойността на променливата, към която сочи
	// Тоест в случая този запис е напълно еквивалентен на y = x;
	y = *px;
	// Следния запис е еквивалентен на py = px; Защо?
	py = &*px;
	// Следния запис е еквивалентен на y = x; Защо?
	y = *&x;
	// Отпечатваме адреса на променливата x на екрана
	printf("The address of X is: %u\n", px);

Обръщения чрез указателите могат да участват във всички изрази, точно както нормалните променливи:

	int x=5, y, *px;
	px = &x;
	y = *px + 10; // y = x + 10;
	*px = 15; // x = 15;
	printf("x = %d", *px); // printf("x = %d", x);
	*px = *px + 5; // x = x + 5;
	y = (*px)++; // y = x + 1;

Както забелязахте от последния оператор в примера е добре при използването на указател в аритметични операции да се използват скоби. В противен случай рискуваме да получим грешка породена от приоритета на операциите.

Остава да си отговорим на въпроса какво ще е получи ако приложим аритметични операции върху самите указатели? Това всъщност е една от силните страни на езика C и се нарича „Адресна аритметика“. При добавянето или изваждането на цяло число към/от указател всяка единица всъщност е равна на броя байтове, определени в паметта за зададения тип на елемента от данните. Например:

	int x, *px;
	px = &x;
	printf("%u\n", px);
	px = px + 9;
	printf("%u\n", px);

От примера ние увеличихме стойността на адреса в паметта (записан в указателя px) със стойност 9 пъти размера на типа данни int. Трябва да сме особно внимателни към какво точно ще „сочи“ указтелят след извършване на такава операция. Адресната аритметика води до смислени резултати само когато данните имат взаимна връзка помежду си. Важно е данните да са подредени последователно в паметта и да са с еднаква мащабируемост. Това обяснява защо указателят трябва да се дефинира строго като указател от определен тип данни. Следният пример демонстрира как това се използва ефикасно при масиви:

	int arr[20];
	int *parr;
	// Указателя "сочи" към първия елемент на масива
	parr = &arr[0];
	// Следното е еквивалентно на arr[2]=12;
	*(parr+2) = 12;
	// Следното е еквивалентно на int z = arr[2];
	int z = *(parr+2);

От примера можем да направим извод, че всички конструкции от вида arr[i] се превръщат в *(parr + i), тоест (parr+i) е адреса на i-тия елемент на масива.

Следният пример демонстрира две функции за сравнение на символни низове. Ако низ s2 „започва“ със символния низ s1, то се връща резултат 1. Ако началото на низ s2 е различно от низа s1 се връща 0:

	int cmpstr(char s1[], char s2[])
	{
		for (int i=0; s1[i]!='\0'; i++)
		{
			if (s2[i]=='\0' || s1[i]!=s2[i]) return 0;
		}
		return 1;
	}

	int pcmpstr(char *s1, char *s2)
	{
		for (int i=0; *s1!='\0'; i++)
		{
			if (*s2=='\0' || *s1 != *s2) return 0;
			s1++;
			s2++;
		}
		return 1;
	}

	void main()
	{
		printf ("%d %d", cmpstr("abc", "xyz"), pcmpstr("abc", "abcdefgh"));
	}

Напълно валидна конструкция е и дефинирането на масиви от указатели. Например:

	int x1, x2, x3;
	int *px[3];
	px[0] = x1;
	px[1] = x2;
	px[2] = x3;

Използването на масиви от указатели и символни низове води до съществено спестяване на памет. Следният пример демонстрира това:

	// Чрез използването на двумерен масив ние сме длъжни да заделим памет
	// за всеки низ, толкова голяма, колкото е най-големият елемент на масива
	char e[3][42] = {
			{"err 1"},
			{"end of line reached"},
			{"fatal error - the program cannot continue!"}
		       }
	// При използването на указатели ние спестяваме тези загуби
	char *pe[3];
	pe[0] = "err 1";
	pe[1] = "end of line reached";
	pe[2] = "fatal error - the proram cannot continue";

Обърнете специално внимание, че „pe[0] = „err1″;“ е инициализация на указател към низ, който се разполага на произволно място в паметта.

Задача: Напишете програма, която чете число от клавиатурата и спрямо него извежда различно съобщение. Съобщенията да се изеждат от масив с указатели къв символни низове.

Задача: Напишете функция, която приема указател към символен низ и определя дължината му.

Задача: Напишете функция, която сравнява дължините на два символни низа и връща указател към по-големия. Използвайте обръщение към функцията в горната задача за улеснение.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*