You're confusing global variables for member variables.
Modifying member variables in member functions is fine, when your function is meant to modify the class state. That's kind of why OOP came about in the first place: to logically group state and behavior. If it's getting hard to keep track of whats happening to your member variables, that's not a problem in using them, the problem is your class is too large.
If you are going to be passing variables around non-stop, you might as well not use any OOP features.
Another thing to consider, is that if modifying member variables was considered unwise, then what would be the point of a setter function?
Let's look at an example of a circular buffer, written in C, swiped from Wiki page:
/* Opaque buffer element type. This would be defined by the application. */
typedef struct { int value; } ElemType;
/* Circular buffer object */
typedef struct {
int size; /* maximum number of elements */
int start; /* index of oldest element */
int end; /* index at which to write new element */
ElemType *elems; /* vector of elements */
} CircularBuffer;
void cbInit(CircularBuffer *cb, int size) {
cb->size = size + 1; /* include empty elem */
cb->start = 0;
cb->end = 0;
cb->elems = calloc(cb->size, sizeof(ElemType));
}
void cbFree(CircularBuffer *cb) {
free(cb->elems); /* OK if null */
}
int cbIsFull(CircularBuffer *cb) {
return (cb->end + 1) % cb->size == cb->start;
}
int cbIsEmpty(CircularBuffer *cb) {
return cb->end == cb->start;
}
/* Write an element, overwriting oldest element if buffer is full. App can
choose to avoid the overwrite by checking cbIsFull(). */
void cbWrite(CircularBuffer *cb, ElemType *elem) {
cb->elems[cb->end] = *elem;
cb->end = (cb->end + 1) % cb->size;
if (cb->end == cb->start)
cb->start = (cb->start + 1) % cb->size; /* full, overwrite */
}
/* Read oldest element. App must ensure !cbIsEmpty() first. */
void cbRead(CircularBuffer *cb, ElemType *elem) {
*elem = cb->elems[cb->start];
cb->start = (cb->start + 1) % cb->size;
}
The C example does pass everything in via arguments, transported around via a struct. This is done because C doesn't offer any built-in OOP features, so that's what you have to do.
If we make that in C++, making everything in the struct a member variable and accessing it 'globally' is entirely appropriate:
class CircularBuffer
{
private:
int size; // maximum number of elements
int start; // index of oldest element
int end; // index at which to write new element
ElemType *elems; // vector of elements
public:
CircularBuffer(int cbSize) {
size = cbSize + 1; // include empty elem
start = 0;
end = 0;
elems = new ElemType[size];
}
~CircularBuffer() {
delete[] elems;
}
bool IsFull() {
return (end+1) % size == start;
}
bool IsEmpty() {
return end == start;
}
void Write(ElemType *elem) {
elems[end] = *elem;
end = (end + 1) % size;
if (end == start)
start = (start + 1) % size; //full, overwrite
}
//Read oldest element. App must ensure !IsEmpty() first.
void Read(ElemType *elem) {
*elem = elems[start];
start = (start + 1) % size;
}
}
If you instead had methods that did something like this:
void Write(ElemType *elem)
{
_Write(ElemType *elem, this->start, this->end, this->size);
}
or worse, had to call the member like so:
cb.Write(&elem,cb.start,cb.end,cb.size);
there would be much rage.
The point is that the variables start, end, size, and elems are part of the internal state of the class, and so those can be manipulated directly inside the classes methods. The user of the class can only manipulate them through the exposed functions, as part of Information Hiding. If you are just going to pass the variables around to all the member functions, like you're writing C, then you might as well use C properly instead of compiling it with a C++ compiler.